Skip to content
Advertisement

React hooks callback ref pattern

I often face this situation with react callbacks:

const MyComponent = ({ onLoad }) => {
  useEffect => {
    // some stuff
    onLoad(/* args */);
  }, [onLoad]);

  return (<div />);
}

The problem is. I consider my component should only load one time. And with useEffect, i have to set onLoad in the dependencies, this cause any change to the onLoad prop to trigger the effect.

I generally solve this issue with a ref

const MyComponent = ({ onLoad: _onLoad }) => {
  const onLoadRef = useRef(_onLoad);
  onLoadRef.current = _onLoad;

  useEffect => {
    // some stuff
    onLoadRef.current(/* args */);
  }, []); // No dependencies anymore

  return (<div />);
}

It works well and solve a lot of similar problems, but i find it a bit ugly, and not really beginner-friendly. I wonder if there is better solutions, or if what i do is an anti-patern ?

Advertisement

Answer

As from the comments above: This is a good resource for how to use useEffect https://reacttraining.com/blog/useEffect-is-not-the-new-componentDidMount/

This article specifically highlights the main reasons on why you need to think of useEffect differently from the Class Component lifecycle methods.

We often times do some setup when the component first mounts like a network call or a subscription. We have taught ourselves to think in terms of “moments in time” with things like componentDidMount(), componentDidUpdate(), and componentWillUnmount(). It’s natural to take that prior knowledge of React and to seek 1:1 equivalents in hooks. I did it myself and I think everyone does at first. Often times I’ll hear in my workshops…

“What is the hooks equivalent to [some lifecycle method]?”

The quick answer is that hooks are a paradigm shift from thinking in terms of “lifecycles and time” to thinking in terms of “state and synchronization with DOM”. Trying to take the old paradigm and apply it to hooks just doesn’t work out very well and can hold you back.

It also gives a good run through of the useEffect and an example of converting from a Class Component to hooks.

Another good source is https://overreacted.io/a-complete-guide-to-useeffect/ from Dan Abramov. I definitely recommend this even though it’s very long to read. It really helped me when I first got started using hooks to think about them the right way.

Here’s a small excerpt from the beginning of the article.

But sometimes when you useEffect, the pieces don’t quite fit together. You have a nagging feeling that you’re missing something. It seems similar to class lifecycles… but is it really? You find yourself asking questions like:

🤔 How do I replicate componentDidMount with useEffect?

🤔 How do I correctly fetch data inside useEffect? What is []?

🤔 Do I need to specify functions as effect dependencies or not?

🤔 Why do I sometimes get an infinite refetching loop?

🤔 Why do I sometimes get an old state or prop value inside my effect?

When I just started using Hooks, I was confused by all of those questions too. Even when writing the initial docs, I didn’t have a firm grasp on some of the subtleties. I’ve since had a few “aha” moments that I want to share with you. This deep dive will make the answers to these questions look obvious to you.

To see the answers, we need to take a step back. The goal of this article isn’t to give you a list of bullet point recipes. It’s to help you truly “grok” useEffect. There won’t be much to learn. In fact, we’ll spend most of our time unlearning.

It’s only after I stopped looking at the useEffect Hook through the prism of the familiar class lifecycle methods that everything came together for me.


In terms of the original question above, using refs is a good way to be able to not have your effect have specific functions and values as dependencies.

In particular they are good if you “you want to read the latest rather than captured value inside some callback defined in an effect”

For this example from the poster:

const MyComponent = ({ onLoad: _onLoad }) => {
  const onLoadRef = useRef(_onLoad);
  onLoadRef.current = _onLoad;

  useEffect => {
    // some stuff
    onLoadRef.current(/* args */);
  }, []); // No dependencies anymore

  return (<div />);
}

This is a completely valid way of doing things, though depending on the args that onLoad takes, and how it works, it might be a good idea to add extra items to the dependency array to make it always in sync.

You could abstract away the wonkiness of the useRef here, but unfortunately the rules of hooks eslint plugin wouldn’t recognize it as a ref. It would work, you’d just need to add the onLoadRef to the dependency array, though it would never cause the effect to re-run. It’s similar to things like dispatch from react-redux where you know it is stable, but the eslint plugin can’t know that.

function useRefUpdater(value) {
  const ref = useRef(value);
  // I forget where I saw that you should change the ref in a useEffect
  useEffect(() => {
    ref.current = value;
  }, [value]);
  return ref;
}
const MyComponent = ({ onLoad: _onLoad }) => {
  const onLoadRef = useRefUpdater(_onLoad)
  useEffect(() => {
    // some stuff
    onLoadRef.current(/* args */);
  }, []); 
     // React Hook useEffect has a missing dependency: 'onLoadRef'. Either include it or remove the dependency array.
  return <div />;
};
User contributions licensed under: CC BY-SA
5 People found this is helpful
Advertisement