Search code examples
react-nativereact-hooksandroid-keypadconditional-rendering

How to hide element when device keyboard active using hooks?


I wanted to convert a hide element when keyboard active HOC I found to the newer react-native version using hooks (useEffect), the original solution using the older react lifecycle hooks looks like this - https://stackoverflow.com/a/60500043/1829251

So I created a useHideWhenKeyboardOpen function that wraps the child element and should hide that child if the device keyboard is active using useEffect. But on render the child element useHideWhenKeyboardOpen isn't displayed regardless of keyboard displayed.

When I've debugged the app I see the following error which I didn't fully understand,because the useHideWhenKeyboardOpen function does return a <BaseComponent>:

ExceptionsManager.js:179 Warning: Functions are not valid as a React child. This may happen if you return a Component instead of from render. Or maybe you meant to call this function rather than return it. in RCTView (at View.js:34)

Question:

How can you attach keyboard displayed listener to a component in the render?

Example useHideWhenKeyboardOpen function:

import React, { useEffect, useState } from 'react';
import { Keyboard } from 'react-native';

// Wrapper component which hides child node when the device keyboard is open.
const useHideWhenKeyboardOpen = (BaseComponent: any) => (props: any) => {
    // todo: finish refactoring.....
    const [isKeyboadVisible, setIsKeyboadVisible] = useState(false);

    const _keyboardDidShow = () => {
        setIsKeyboadVisible(true);
    };

    const _keyboardDidHide = () => {
        setIsKeyboadVisible(false);
    };

    /**
     * Add callbacks to keyboard display events, cleanup in useeffect return.
     */
    useEffect(() => {
        console.log('isKeyboadVisible: ' + isKeyboadVisible);
        Keyboard.addListener('keyboardDidShow', _keyboardDidShow);
        Keyboard.addListener('keyboardDidHide', _keyboardDidHide);

        return () => {
            Keyboard.removeCurrentListener();
        };
    }, [_keyboardDidHide, _keyboardDidShow]);

    return isKeyboadVisible ? null : <BaseComponent {...props}></BaseComponent>;
};

export default useHideWhenKeyboardOpen;

Example Usage:

return(
.
.
.
 {useHideWhenKeyboardOpen(
                        <View style={[styles.buttonContainer]}>
                            <Button
                                icon={<Icon name="save" size={16} color="white" />}
                                title={strings.STOCKS_FEED.submit}
                                iconRight={true}
                                onPress={() => {
                                    toggleSettings();
                                }}
                                style={styles.submitButton}
                                raised={true}
                            />
                        </View>,
                    )}

)

Solution

  • Mindset shift will help: think of hooks as data source rather than JSX factory:

    const isKeyboardShown = useKeyboardStatus();
    
    ...
    {!isKeyboardShown && (...
    

    Accordingly your hook will just return current status(your current version look rather as a HOC):

    const useHideWhenKeyboardOpen = () => {
        const [isKeyboadVisible, setIsKeyboadVisible] = useState(false);
    
        const _keyboardDidShow = useCallback(() => {
            setIsKeyboadVisible(true);
        }, []);
    
        const _keyboardDidHide = useCallback(() => {
            setIsKeyboadVisible(false);
        }, []);
    
        useEffect(() => {
            Keyboard.addListener('keyboardDidShow', _keyboardDidShow);
            Keyboard.addListener('keyboardDidHide', _keyboardDidHide);
    
            return () => {
              Keyboard.addListener('keyboardDidShow', _keyboardDidShow);
              Keyboard.addListener('keyboardDidHide', _keyboardDidHide);
            };
        }, [_keyboardDidHide, _keyboardDidShow]);
    
        return isKeyboadVisible;
    };
    

    Note usage of useCallback. Without it your hook will unsubscribe from Keyboard and subscribe again on every render(since _keyboardDidHide would be referentially different each time and would trigger useEffect). And that's definitely redundant.