A deep dive on forms with modern React
React 19 introduces terrific primitives for building great forms. Let's dive deep on forms for the web with 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.
React 19 introduces terrific primitives for building great forms. Let's dive deep on forms for the web with React.
Understanding server-side waterfalls with RSCs and client-side waterfalls we're familiar with and why server-side waterfalls are probably better.
Speed up your app's loading of code/data/assets with "render as you fetch" with and without React Suspense for Data Fetching
If you use a ref in your effect callback, shouldn't it be included in the dependencies? Why refs are a special exception to the rule!