How to type a React form onSubmit handler
Excellent TypeScript definitions for your React forms
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.
Excellent TypeScript definitions for your React forms
I still remember when I first heard about React. It was January 2014. I was listening to a podcast. Pete Hunt and Jordan Walke were on talking about this framework they created at Facebook that with no support for two way data-binding, no built-in HTTP library, no dependency injection, and in place of templates it had this weird XML-like syntax for the UI. And to top it all off, I was listening to it while driving to the first ever ng-conf.
React Server Components are going to improve the way we build web applications in a huge way... Once we nail the abstractions...
A basic introduction memoization and how React memoization features work.