reactjsreact-nativereact-hookslifecycle

Is there a function that returns a promise which gets resolved when any pending changes to a component have been applied in React Native?


I want to show a custom input component and then call its method on a button click:

const Parent = () => {
  const customInputRef = useRef(null);

  const [customInputVisible, setCustomInputVisible] = useState(false);

  async function onPress() {
    setCustomInputVisible(true);

    await resolvePendingChanged(); // customInput is not null and can be accessed

    customInputRef.current.customMethod();
  }

  return (
    <View>
      <Button onPress={onPress}>Press me!</Button>

      {customInputVisible && <CustomInput ref={customInputRef} />}
    </View>
  );
}

I saw that people use a custom forceUpdate function in order to trigger a component update but that didn't really help in my case.

In Svelte there's this "tick" lifecycle hook that does exactly what I need.

It returns a promise that resolves as soon as any pending state changes have been applied to the DOM (or immediately, if there are no pending state changes).

Is there an equivalent of Svelte's tick in React and if not how can I solve this problem in React?


Solution

  • You can create a custom hook that uses a callback ref to set the actual ref, and resolve a promise:

    const { forwardRef, useImperativeHandle, useRef, useState, useCallback, useMemo } = React;
    
    const CustomInput = forwardRef((props, ref) => {
      const inputRef = useRef();
      
      useImperativeHandle(ref, () => ({
        customMethod: () => {
          inputRef.current.focus();
        }
      }), []);
      
      return <input ref={inputRef} />;
    });
    
    class Deferred {
      constructor() {
        this.promise = new Promise((resolve, reject) => {
          this.resolve = resolve;
          this.reject = reject;
        });
      }
    }
    
    const waitForComponent = () => {
      const componentRef = useRef(null);
     
      return useMemo(() => {
        let deferred = new Deferred();
        
        return {
          waitLoad(ref) {
            componentRef.current = ref;
            
            if (ref) deferred.resolve();
            else deferred = new Deferred(); // create new Promise when ref is null
          },
          isLoaded: () => deferred.promise,
          componentRef
        };
      }, []);
    }
    
    const Parent = () => {
      const { waitLoad, componentRef, isLoaded } = waitForComponent();
      const [customInputVisible, setCustomInputVisible] = useState(false);
    
      function onPress() {
        setCustomInputVisible(visible => !visible);
         
        // use async here - SO snippet doesn't support async await
        isLoaded().then(() => componentRef.current.customMethod());
      }
    
      return (
        <div>
          <button onClick={onPress}>Press me!</button>
    
          {customInputVisible && <CustomInput ref={waitLoad} />}
        </div>
      );
    };
    
    ReactDOM.render(
      <Parent />,
      root
    );
    <script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script>
    <script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
    
    <div id="root"></div>