Just Use React

In my post "How React Uses Closures to Avoid Bugs" I explain some of the trade-offs React made when they switched from classes and lifecycles to functions and hooks. I want to dig a little deeper on this topic.
In that post, I share the following example:
function useDebounce(callback, delay) { const callbackRef = React.useRef(callback) React.useLayoutEffect(() => { callbackRef.current = callback }) return React.useMemo( () => debounce((...args) => callbackRef.current(...args), delay), [delay], )}
I want to talk a bit about this pattern that my friend Yago (who created the original hook) likes to call "The Latest Ref Pattern."
The pattern itself is pretty simple. Here's the part that's the pattern:
const callbackRef = React.useRef(callback)React.useLayoutEffect(() => { callbackRef.current = callback})
useLayoutEffect
?
That's it. That's the pattern.
So why would you want to do this? Well, let's think about when you use useRef
. You use useRef
whenever you want to keep track of a value, but not trigger a re-render when you update it. So in our case, we're trying to keep track of callback
. The reason for this, is we want to make sure that we're always calling the latest version of the callback
rather than one from an old render.
But why don't we use useState
instead? Could we keep track of this latest callback value in an actual state value? We don't want to use useState
because we don't need to trigger a component re-render when we update to the latest value. In fact, in our case if we tried, we'd trigger an infinite loop (go ahead, try it đ).
And because we don't need or want a re-render when we update the callback
to the latest value, it means we also don't need to (and really shouldn't) include it in a dependency array for useEffect
, useCallback
, or in our case useMemo
. This is an important point, so I want to dive into it a bit.
It's really important that you follow the eslint-plugin-react-hooks/exhaustive-deps
rule and always include all dependencies. But you should skip the current
value of a ref. So don't ever do this:
// â don't ever do thisReact.useEffect(() => {}, [ref.current])
This is because updating a ref doesn't trigger a re-render anyway, so React can't call the effect callback or update memoized values when the ref is updated. So if you include ref.current
in the dependency array, you'll get surprising behavior that's difficult to debug. As a side-note, because the ref
itself is a stable object, it doesn't make a difference if you include the ref
object itself in your dependency array:
// đ¤ˇââď¸ doesn't make a difference whether you include the ref or not.React.useEffect(() => {}, [ref])
You can run into some serious bugs if you don't include all your non-ref deps though, so just please, don't ignore the linting rule for this.
Before using "the latest ref pattern" everywhere, I suggest you get a good understanding for what it is you're side-stepping, so if you haven't already, give "How React Uses Closures to Avoid Bugs" a read-through. That'll help you get a better idea of when it can be useful to use this particular pattern. I'd love to hear situations when you find this pattern useful. Tweet @ me.
Take care đ
Delivered straight to your inbox.
Make your React apps feel instant with the new useOptimistic hook from React 19. Improve UX by updating UI immediately while async actions complete.
Truly maintainable, flexible, simple, and reusable components require more thought than: "I need it to do this differently, so I'll accept a new prop for that". Seasoned React developers know this leads to nothing but pain and frustration for both the maintainer and user of the component.
React components should not modify props directly. Learn why encapsulation is important and explore different approaches to handle props correctly in React, ensuring your components remain reusable and your application's data flow stays predictable.