Ref objects inside useEffect Hooks

When making a custom hook that needs a reference to a DOM node, your first guess would be to use the new useRef hook. It was certainly mine.

So naturally I went with that for my first implementation of Hooks in react-intersection-observer — But then an interesting issue came up, that I hadn’t considered.

Essentially, I was assuming that a ref would always be defined, but —

  • ref could be undefined to begin with, and then set later.
  • ref could change to a different element.
  • ref could change from an element to undefined.

My hook wasn’t handling any of these cases.

The problem with refs and useEffect

The useRef hook can be a trap for your custom hook, if you combine it with a useEffect that skips rendering. Your first instinct will be to add ref.current to the second argument of useEffect, so it will update once the ref changes.

But the ref isn’t updated till after your component has rendered — meaning, any useEffect that skips rendering, won’t see any changes to the ref before the next render pass.

An example of the problem in CodeSandbox

Looking at the community created hooks, this seems like a fairly widespread issue right now. Great Hook collections like and palmerhq/the-platform falls into this trap — most of the time because they assume the ref won’t change, and it’s set during the first render.

I went ahead and asked Dan Abramov how to solve this issue:

The React team did try to warn us about errors if we skip effects.

The solution

Instead of useRef, you should create a ref callback with useCallback.

Once the callback is triggered with a node, you can consume it as needed.

For a complete example, have a look at the useInView source code for react-intersection-observer.

Official Solution

The React FAQ has now been updated with an example of using a ref callback to measure the size of DOM elements. This is also a solution to this exact problem, so make sure to read that entry.

You could argue that hooks can assume the ref is set when created, but I’ve already seen multiple cases of developers running into this issue, unsure of why their hook isn’t working. Seeing how simple the solution is, I think it’s important that all custom hooks start to implement it.

Keep in mind this is only a problem if your useEffect hook doesn’t trigger on all renders, so keep using useRef in those hooks.

Frontend Tech Lead at Charlie Tango

Frontend Tech Lead at Charlie Tango