2

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?

Yulian
  • 6,262
  • 10
  • 65
  • 92

1 Answers1

2

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>
Ori Drori
  • 183,571
  • 29
  • 224
  • 209
  • 1
    Thanks a lot for your response! However, this will only work if you want to always call `customMethod()` whenever `customInputVisible` is `true`. However, that's not my case. I want to make sure that `customMethod()` is only called `onPress` and not when `customInputVisible` changes. – Yulian Apr 16 '21 at 13:02
  • 1
    It works, thanks a lot! It's a super smart solution, although I'm quite surprised that you have to write so much logic for such a simple task. This is solved with a single line in both Svelte and Angular. Still can't understand why React is the most popular front-end library. – Yulian Apr 16 '21 at 14:17
  • 1
    Because it's an edge case in React, that you rarely encounter. In addition, now you have a reusable hook, that you can use if you encounter similar problems in the future. – Ori Drori Apr 16 '21 at 14:25
  • 2
    Hm, maybe you're right, although looks pretty common to me. I had to do this in a few different projects, for example, to focus a nested input or in this case I had to add a user tag (@user) when a "Reply" button is clicked. Anyway, thanks a lot again! Saved me lots of time! – Yulian Apr 16 '21 at 14:29