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 beundefined
to begin with, and then set later.ref
could change to a different element.ref
could change from an element toundefined
.
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.
Looking at the community created hooks, this seems like a fairly widespread issue right now. Great Hook collections like usehooks.com 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.