DEV Community

Cover image for Fixing Keyboard Avoiding in React Native
iway1
iway1

Posted on

Fixing Keyboard Avoiding in React Native

I love React Native, but one thing that has always felt overly difficult to get right in my applications is implementing keyboard avoiding behavior. So, maybe it's time we make it easier for everyone.

And I know what you may be thinking, React Native has a KeyboardAvoidingView. Just use that, right? that's easy enough?

I wish it was that simple.

React Native's biggest mistake?

The KeyboardAvoidingView can work well sometimes, but there are many layouts where it simply just doesn't work (and probably never will). Plus, it's inconsistent across platforms.

Let's take a look at some simple layout:

function SomeSimpleLayout() {
    return (
        <View style={{
            flex: 1,
        }}>
            <View
                style={{
                    height: Dimensions.get('window').height - 300,
                    width: '100%',
                    justifyContent: 'flex-start'
                }}
            />
            <KeyboardAvoidingView>
                <TextInput
                    style={textInputStyle}
                />
                <TextInput
                    style={textInputStyle}
                />
            </KeyboardAvoidingView>
        </View>
    )
}
Enter fullscreen mode Exit fullscreen mode

This is pretty similar to a design I had to implement at my consulting job recently.

It just doesn't work. Yes, you heard me right, React Native's own KeyboardAvoidingView can't avoid the keyboard in this very simple situation.

Broken Keyboard Avoiding

Maybe they should call it the KeyboardSometimesMaybeAvoidingIfItFeelsLikeItView? To add insult to injury, when it does work it doesn't provide consistency across platform.

You may be thinking, did you try all the different values for the behavior prop. Yes, I did. They didn't work.

Frustrated

And even if they did work it would still suck, because a KeyboardAvoidingView should just avoid the dang keyboard. It shouldn't make me have to remember and think about implementation details.

This is something I've dealt with from time to time throughout the entire course of my React Native consulting career, and it always felt so bad.

Why can't we just have a view that avoids the keyboard consistently? Is keyboard avoiding that hard?

Well, is it really that hard?

The answer is... No. It really isn't. React Native just screwed the pooch on this one.

Again, I ❤️love❤️ React Native. It's an amazing tool and has allowed me to build some amazing things really quickly. But this component freaking sucks. The API is bad, it doesn't handle all use cases, it's just clunky and provides a bad developer experience.

What makes it worse is that this is included in React Native itself, which means developers are likely to choose it as their first solution!

Think about the number of hours that have gone into tweaking layouts to get the KeyboardAvoidingView to work properly on iOS, only to have it behave unexpectedly on Android?

There's got to be a better way. We can fix this, we're engineers.

The solution

Using the power of basic geometry and knowledge of React Native, we can build a keyboard avoiding view that works in every layout. A keyboard avoiding view that actually avoids the keyboard all the time. Let's call it the, uhhh... KeyboardAvoiderView! 😬

And it will avoid the keyboard 😁:

Actually Works

Same layout as before, only now KeyboardAvoiderView avoiders the keyboard. Nice! And it will work in any layout, because it uses transforms (which are independent of the layout) instead of animating properties that affect that layouts

Also, it has the same behavior on both platforms:

Cross Platform Consistency

Awesome. That's what we wanted. How does it work? Well you can view the full source code of the component I made to do this here if you want, but that's probably not as useful for you to know as knowing the key implementation details of the Component:

KeyboardAvoiderView key implementation details

  • React Native allows us to get the currently focused text input from TextInput.State.currentlyFocusedInput(), and we can measure that input with .measure():
function MyComponent() {
    function handleKeyboardShow(e) {
        const input = TextInput.State.currentlyFocusedInput();
        if(!input) return;
        input.measure((x, y, width, height, pageX, pageY)=>{
            const inputBottom = (pageY + height);
        })
    }
}
Enter fullscreen mode Exit fullscreen mode

That's actually really cool, and is required for implementing keyboard avoiding behavior. Any time we want a ref to the currently focused input we can just call that function.

  • The keyboard end coordinate is passed to keyboard event handlers:
function MyComponent() {
    function handleKeyboardShow(e) {
        const topOfKeyboard = e.endCoordinates.screenY;
    }
}
Enter fullscreen mode Exit fullscreen mode
  • Android has system keyboard behavior that cannot be disabled, so we have to calculate that and compensate for it:
function MyComponent() {
    function handleKeyboardShow(e) {
        const input = TextInput.State.currentlyFocusedInput();
        if(!input) return;
        input.measure((x, y, width, height, pageX, pageY)=>{
            const inputBottom = (pageY + height);
            // How much the system panned by
            const androidPannedBy = Math.max(inputBottomY - e.endCoordinates.screenY, 0);
        })
    }
}
Enter fullscreen mode Exit fullscreen mode

Here we calculate the distance Android would pan the screen when the keyboard shows. It'd be nice if Android let us disable the system behavior, but it doesn't. That's okay. We know math.

Now, we can just calculate the amount we should move based on these values:

var weShouldScrollBy = inputBottom - extraSpace - topOfKeyboard;
if(Platform.OS == 'android') weShouldScrollBy += androidPannedBy;
Enter fullscreen mode Exit fullscreen mode

Notice the extraSpace variable, that's the distance we want to have between the input and the keyboard. If we didn't have that, we wouldn't even need to perform any calculations on Android, but remember we want cross platform consistency!

We won't dive too much into the code , but the full KeyboardAvoiderView is available in my package @good-react-native/keyboard-avoider(Yes I'm shilling my own library in this post, but only because I do think we can solve a real problem here).

What about scroll views?

Some times we have text inputs in scroll views. In these cases, we just use react-native-keyboard-aware-scroll-view, yeah?

Not so fast. react-native-keyboard-aware-scroll-view is a very useful library, it works and the api is good.

Unfortunately, it has some considerable shortcomings. I go into detail about them in my package repo, but the main issues are the fact that it simply breaks in some situations and lacks responsiveness/consistency.

Like KeyboardAwareScrollView, react-native-keyboard-aware-scroll-view doesn't work with all layouts. If it's not fullscreen, it breaks:

Full Screen

Also, it will bug out any time you're using react navigation. (Yes this bug still exists, no you cannot avoid it in all situations).

It's not maximally responsive:

Less Responsive

Notice the input getting covered up. This isn't extremely noticeable but it is worse than if the input was able to avoid the keyboard entirely (IMO).

Plus, it doesn't match the behavior of the KeyboardAvoidingView. (it uses the ScrollView's scrollTo method instead of handling scrolling itself). There are more issues as well, but I think you'll get the point by now.

And by the way, I really appreciate the devs at react-native-keyboard-aware-scroll-view for contributing to the community. If I sound upset, it's only because I'm scarred from all of the issues this library has caused me and my clients, and because I know that it could be better.

We're engineers, we can improve things. There's always a better way. And that better way is called, uh... KeyboardAvoiderScrollView?

Another member of @good-react-native/keyboard-avoider. This scroll view has behavior consistent with the KeyboardAvoiderView, is more responsive, and wont break based on the rest of the screen.

How is KeyboardAvoiderView doing this you might ask? The main reasons are that:

  • It uses an animation instead of scrolling the scroll view with the built in animation, which allows for greater control of the animation.
  • It uses absolute page measurements (I'm not sure why, but react-native-keyboard-aware-scroll-view uses window measurements, which may be why it breaks).

Anyways

Anyways, that's it. I made a package that seems to be a significant improvement for all types of keyboard avoidering behavior compared with what people typically use in React Native (KeyboardAvoidingView and react-native-keyboard-aware-scroll-view).

There's even a <KeyboardAvoiderInsets/> component for when you do want to alter your layout when the keyboard shows.

Again, you can check it out here. It's still in its early stages so it's definitely got a few kinks to work out, so any community contributions would be awesome.

I especially would love to hear out any feature requests. Right now there are some props that react-native-keyboard-aware-scroll-view has that our library doesn't have, so if you need one of those you just can't use our library (yet).

I'd love to know which of those features people actually want so that we can get them added in.

The package isn't perfect, surely there are some issues and bugs that have yet to surface. But ultimately its implementation means it's just a lot easier to use and can provide a better developer experience than other solutions.

Other packages aren't going to "catch up", so to speak, so I think in this rare circumstance it might be time to start fresh.

Either way, thanks so much for your time. 😊

Top comments (0)