skip to Main Content

I have created my own modal component based on react-native-modal with "controls" through useRef. This is my code.

import React, {useState, forwardRef, useImperativeHandle} from 'react';
import RNModal from 'react-native-modal';

// <Modal />
export default Modal = forwardRef(({children, ...props}, ref) => {
    const [isVisible, setIsVisible] = useState(false);

    useImperativeHandle(
        ref,
        () => ({
        show: () => {
        setIsVisible(true);
        },
        hide: () => {
        setIsVisible(false);
        },
        }),
        [],
    );

    const childrenWithProps = React.Children.map(children, child => {
        return React.cloneElement(child, {isMounted: isVisible});
    });

    return (
        <RNModal
            isVisible={isVisible}
            animationInTiming={200}
            animationOutTiming={200}
            iseNativeDriver={true}
            hideModalContentWhileAnimating={true}
            onBackdropPress={ref.current?.hide}
            onBackButtonPress={ref.current?.hide}
        {...props}>
            {childrenWithProps}
        </RNModal>
    );
});

As you can see, I’m trying to pass the "isMounted" prop through React.cloneElement. This way, I can control useEffect functions to run whenever the modal opens or closes.

The problem is: this doesn’t work. When I have code like this, it doesn’t work.

const MyModal = forwardRef(({isMounted, ...props}, ref) => {

    useEffect(() => {
        console.log(isMounted); 
    }, [isMounted]);

    return (
        <Modal ref={ref}>
            <View>...</View>
        </Modal>
    );
});

What is wrong with my code?

2

Answers


  1. I suspect the React.cloneElement is working.

    Perhaps your refs setup is a little muddled?

    Check this code – it may be what you intended:

    import React, {
      useState,
      forwardRef,
      useImperativeHandle,
      useEffect,
      useRef,
      ReactNode,
    } from 'react';
    import {Text, Button} from 'react-native';
    import RNModal from 'react-native-modal';
    
    interface ModalProps {
      children: ReactNode;
    }
    
    type ModalRefApi = {show: () => void; hide: () => void};
    /**
     * Modal that exposes a ref api
     */
    const ModalWithRefApi = forwardRef(({children}: ModalProps, outerRef) => {
      const [isVisible, setIsVisible] = useState(false);
      useImperativeHandle<unknown, ModalRefApi>(outerRef, () => ({
        show: () => setIsVisible(true),
        hide: () => setIsVisible(false),
      }));
    
      return (
        <RNModal isVisible={isVisible} onBackdropPress={() => setIsVisible(false)}>
          {React.Children.map(children, (child, i) =>
            React.cloneElement(child as React.ReactElement, {
              text: `PROOF THAT GETS CLONED VALUE ${i}`,
            }),
          )}
        </RNModal>
      );
    });
    
    interface MountableProps {
      text?: boolean;
    }
    
    /**
     * A child that proves its getting cloned props
     */
    const MountableChild: React.FC<MountableProps> = ({
      text = '(empty by default)',
    }) => {
      useEffect(() => {
        console.log('mounted child');
        return function cleanUp() {
          console.log('unmounted child');
        };
      }, []);
      return <Text style={{color: 'white'}}>{text}</Text>;
    };
    
    /**
     * Perhaps your intended usage?
     */
    const UsingModalWithChildAndControls: React.FC = () => {
      const modalRefApi = useRef<ModalRefApi>(null);
    
      return (
        <>
          <Button onPress={() => modalRefApi.current?.show()} title="show" />
          <ModalWithRefApi ref={modalRefApi}>
            <MountableChild />
            <MountableChild />
            <MountableChild />
          </ModalWithRefApi>
        </>
      );
    };
    
    export default UsingModalWithChildAndControls;
    
    

    A few points:

    • Children are unmounted when visible:false
    • I "cloned in" the text value of the children so you can see cloneElement working
    • I changed the RNModel prop onBackdropPress, before it tried to call the imperative API, which is exposed for parents. Instead I just used the internal created setIsVisisble state handler
    Login or Signup to reply.
  2. The issue with your code is that you’re passing the isMounted prop to the Modal component, but then you’re not passing it down to the actual modal content (childrenWithProps). Therefore, the MyModal component doesn’t receive the isMounted prop.

    To fix this, you need to make sure that the isMounted prop is passed down to the child components inside the Modal. You can modify the useImperativeHandle block in your Modal component to achieve this:

    // ...
    
    useImperativeHandle(
      ref,
      () => ({
        show: () => {
          setIsVisible(true);
        },
        hide: () => {
          setIsVisible(false);
        },
      }),
      [setIsVisible],
    );
    
    const childrenWithProps = React.Children.map(children, child => {
      return React.cloneElement(child, { isMounted: isVisible });
    });
    
    // ...
    

    Now, the isMounted prop should be passed down to the child components within the Modal. Ensure that your MyModal component is correctly using the isMounted prop:

    const MyModal = forwardRef(({ isMounted, ...props }, ref) => {
      useEffect(() => {
        console.log(isMounted);
      }, [isMounted]);
    
      return (
        <Modal ref={ref}>
          <View>...</View>
        </Modal>
      );
    });
    

    With these changes, the useEffect inside MyModal should correctly log the isMounted prop when the modal is opened or closed.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search