A simple, reusable keyboard avoiding animation hook, with TypeScript & Reanimated V2!
Tags: reactnative, typescript, reacthooks, reanimated
Published: 2021-07-31 17:44:04

---
title: A simple, reusable keyboard avoiding animation hook, with TypeScript & Reanimated V2!
published: true
description: 
tags: reactnative, typescript, reacthooks, reanimated
cover_image: https://images.unsplash.com/photo-1516382799247-87df95d790b7?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=2106&q=80
---

If you are developing app in React Native for quite some time now, you probably noticed that one of the most hard to work with component is the `KeyboardAvoidingView`. 

Even the pros are guilty about this, here's a 1-on-1 talk of William Candillon and Jonny burger on the challenges on working with `KeyboardAvoidingView`.

{% youtube SskI4fP03cA %}

There are packages like [react-native-keyboard-aware-scrollview](https://github.com/APSL/react-native-keyboard-aware-scroll-view) (just in case you are not "aware") that  automatically scrolls to the focused `TextInput` component. But, in your use case, that is not enough. You probably want to push an element on top of the soft-keyboard or change style when it is visible or hidden.

Well, you've come to the right place! Today, I'll show you how to do it!

> If you are new to `react-native-reanimated` v2, I recommend this amazing [video](https://www.youtube.com/watch?v=dowHt1dckDU&t=9s)!

I recommend to type the code instead of just copy and pasting, it will help you in learning. But if you are impatient, you can go ahead and check the final result and then copy and paste the example usage.

Let's get started!

### Boilerplate

Copy and paste this boilerplate code to speed things up.

```ts
import {useCallback, useEffect} from 'react';
import {Keyboard, KeyboardEventListener, ScreenRect} from 'react-native';
import Animated, {
  useAnimatedStyle,
  useSharedValue,
  withTiming,
} from 'react-native-reanimated';

export const useKeyboardAvoiding = () => {

};
```

### Animated shared values

We need a way to track the keyboard visibility state and coordinates, so we will add two reanimated shared values. Shared values are "shared" because they run on the UI and JS thread. If you are not familiar with this, [RN docs has a nice overview on this topic](https://reactnative.dev/docs/performance#what-you-need-to-know-about-frames).

`keyboardVisible`, as the name implies, will hold the visibility state of the keyboard.

`keyboardEndCoordinates` will hold the coordinates, like `x` & `y` axis, and `height` after the keyboard is displayed or hidden.

Add these variables inside the hook:
```ts
// other codes from prev step
export const useKeyboardAvoiding = () => {
  const keyboardVisible = useSharedValue(false);
  const keyboardEndCoordinates = useSharedValue<ScreenRect | null>(null);
};
```

### Listening to keyboard events

Now, let's add the `keyboardWillChangeFrame` and `keyboardWillHide` listeners.

We will add them on mount, and remove the listeners on unmount using `useEffect` and then wrap the callbacks with `useCallback`.

Listening to `keyboardWillChangeFrame`:

```ts
export const useKeyboardAvoiding = () => {
  // other codes from prev step
  const handleKeyboardWillChangeFrame = useCallback<KeyboardEventListener>(
    () => {},
    [],
  );

  useEffect(() => {
    const emitter = Keyboard.addListener(
      'keyboardWillChangeFrame',
      handleKeyboardWillChangeFrame,
    );

    return () => emitter.remove();
  }, [handleKeyboardWillChangeFrame]);

};
```

Listening to `keyboardWillHide`:
```ts
export const useKeyboardAvoiding = () => {
  // other codes from prev step
  const handleKeyboardWillHide = useCallback<KeyboardEventListener>(
    () => {},
    [],
  );

  useEffect(() => {
    const emitter = Keyboard.addListener(
      'keyboardWillHide',
      handleKeyboardWillHide,
    );

    return () => emitter.remove();
  }, [handleKeyboardWillHide]);

};
```

### Handling keyboard events

We will now store the keyboard state into the shared values.

In order to assign a value to the shared values, we will need to assign it to the `value` property.

Modify the `handleKeyboardWillChangeFrame` and `handleKeyboardWillHide` callbacks:

```ts
  const handleKeyboardWillChangeFrame = useCallback<KeyboardEventListener>(
    ({endCoordinates}) => {
      keyboardVisible.value = true;
      keyboardEndCoordinates.value = endCoordinates;
    },
    [keyboardEndCoordinates.value, keyboardVisible.value],
  );


  const handleKeyboardWillHide = useCallback<KeyboardEventListener>(
    ({endCoordinates}) => {
      keyboardVisible.value = false;
      keyboardEndCoordinates.value = endCoordinates;
    },
    [keyboardEndCoordinates.value, keyboardVisible.value],
  );
```

### Animating the style

And now, the most exciting part of the tutorial!

Let's create an animated style based on the shared values. To create an animated style in reanimated v2, we will use the `useAnimatedStyle` hook. We will also use the `withTiming` higher-order function to nicely animate the transition.

By default, we will animate the `bottom` style, but we will implement a way to customize this later. 

We will use 90% of the keyboard height. It seems to have an extra offset to the height that will cause the component being animated to be pushed further from the top of the keyboard.

```ts
// add this below the shared value declarations
// and before any of the callbacks

  const animatedStyle = useAnimatedStyle(() => {
    const kbHeight = keyboardEndCoordinates.value?.height ?? 0;

    return {
      bottom: withTiming(keyboardVisible.value ? kbHeight * 0.9 : 0),
    };
  }, [keyboardEndCoordinates, keyboardVisible]);
```

This is now ready to be used. But, it's not very flexible because it is fixed to animate the `bottom` style. The next step is to add a way to customize this.

### Customizing the animated style

The final step is to allow the hook to accept a custom function that returns the custom style. We need to use a [`worklet`](https://docs.swmansion.com/react-native-reanimated/docs/worklets/) function in order for this to work.

A `worklet` function is run on the UI thread by reanimated V2. We will need to add a `'worklet'` directive at the very top of the function block.

Let's modify the hook to accept a `animatedStyleWorkletFn` function and then we will use it inside the `useAnimatedStyle` hook

```ts
// imported files are here

export type KeyboardAvoidingStyleWorkletFn = (
  endCoordinates: Animated.SharedValue<ScreenRect | null>,
  isVisible: Animated.SharedValue<boolean>,
) => ReturnType<typeof useAnimatedStyle>;


export const useKeyboardAvoiding = (
  animatedStyleWorkletFn?: KeyboardAvoidingStyleWorkletFn,
) => {
  const keyboardVisible = useSharedValue(false);
  const keyboardEndCoordinates = useSharedValue<ScreenRect | null>(null);

  const animatedStyle = useAnimatedStyle(() => {
    const kbHeight = keyboardEndCoordinates.value?.height ?? 0;

    return animatedStyleWorkletFn
      ? animatedStyleWorkletFn(keyboardEndCoordinates, keyboardVisible)
      : {
          bottom: withTiming(keyboardVisible.value ? kbHeight * 0.9 : 0),
        };
  }, [animatedStyleWorkletFn, keyboardEndCoordinates, keyboardVisible]);


   // callbacks are here

   return {keyboardVisible, keyboardEndCoordinates, animatedStyle};
};
```

### Final result


```tsx
import React from 'react';
import {useCallback, useEffect} from 'react';
import {
  Keyboard,
  KeyboardEventListener,
  ScreenRect,
  StyleSheet,
  TextInput,
  View,
  Button,
} from 'react-native';
import Animated, {
  useAnimatedStyle,
  useSharedValue,
  withTiming,
} from 'react-native-reanimated';

export type KeyboardAvoidingStyleWorkletFn = (
  endCoordinates: Animated.SharedValue<ScreenRect | null>,
  isVisible: Animated.SharedValue<boolean>,
) => ReturnType<typeof useAnimatedStyle>;

export const useKeyboardAvoiding = (
  animatedStyleWorkletFn?: KeyboardAvoidingStyleWorkletFn,
) => {
  const keyboardVisible = useSharedValue(false);
  const keyboardEndCoordinates = useSharedValue<ScreenRect | null>(null);

  const animatedStyle = useAnimatedStyle(() => {
    const kbHeight = keyboardEndCoordinates.value?.height ?? 0;

    return animatedStyleWorkletFn
      ? animatedStyleWorkletFn(keyboardEndCoordinates, keyboardVisible)
      : {
          bottom: withTiming(keyboardVisible.value ? kbHeight * 0.9 : 0),
        };
  }, [animatedStyleWorkletFn, keyboardEndCoordinates, keyboardVisible]);

  const handleKeyboardWillChangeFrame = useCallback<KeyboardEventListener>(
    ({endCoordinates}) => {
      keyboardVisible.value = true;
      keyboardEndCoordinates.value = endCoordinates;
    },
    [keyboardEndCoordinates, keyboardVisible],
  );

  const handleKeyboardWillHide = useCallback<KeyboardEventListener>(
    ({endCoordinates}) => {
      keyboardVisible.value = false;
      keyboardEndCoordinates.value = endCoordinates;
    },
    [keyboardEndCoordinates, keyboardVisible],
  );

  useEffect(() => {
    const emitter = Keyboard.addListener(
      'keyboardWillChangeFrame',
      handleKeyboardWillChangeFrame,
    );

    return () => emitter.remove();
  }, [handleKeyboardWillChangeFrame]);

  useEffect(() => {
    const emitter = Keyboard.addListener(
      'keyboardWillHide',
      handleKeyboardWillHide,
    );

    return () => emitter.remove();
  }, [handleKeyboardWillHide]);

  return {keyboardVisible, keyboardEndCoordinates, animatedStyle};
};

export const DefaultStyle = () => {
  const {animatedStyle} = useKeyboardAvoiding();

  return (
    <View style={styles.containerStyle}>
      <TextInput multiline style={styles.input} />
      <Animated.View style={animatedStyle}>
        <Button title="Save" onPress={() => {}} />
      </Animated.View>
    </View>
  );
};

export const CustomStyle = () => {
  const {animatedStyle} = useKeyboardAvoiding((endCoords, isVisible) => {
    'worklet';
    const height = endCoords.value?.height ?? 0;

    return {
      marginBottom: withTiming(isVisible.value ? height * 0.7 : 0),
    };
  });

  return (
    <Animated.View style={[styles.containerStyle, animatedStyle]}>
      <TextInput multiline style={styles.input} />
      <Button title="Save" onPress={() => {}} />
    </Animated.View>
  );
};

const styles = StyleSheet.create({
  containerStyle: {
    flex: 1,
  },
  input: {
    height: 500,
  },
});

```

I hope you enjoyed this tutorial! This is my first tech blog so I apologize for the rough edges. If you have any tutorial suggestions, please comment down below. Thank you!