DEV Community

Cover image for React.js ~The Latest Ref Pattern ~
Ogasawara Kakeru
Ogasawara Kakeru

Posted on

React.js ~The Latest Ref Pattern ~

This pattern holds callback passed to a customhook in the ref and update existing value everytime rendered to have the latest value.

function useDebounce(callback, delay) {
  const callbackRef = React.useRef(callback)

  React.useLayoutEffect(() => {
    callbackRef.current = callback
  })

  return React.useMemo(
    () => debounce((...args) => callbackRef.current(...args), delay),
    [delay],
  )
}
Enter fullscreen mode Exit fullscreen mode

This pattern is named by Yago Pereria. The above example describes the pattern by using useDebounce. Calling callbackref.current in the debounced function, the old function closed by closure is not called, but the latest callback is always called.

Why is ref used?
Because you need to have a latest value without re-rendering. If you hold the value in state, the component renderes everytime update the value and invokes infinite loop.

Dealing with Dependency Array

This is the main focus of the second half of the article. Eventhough you have to obey the exhaustive-deps rule basically, you must not place current of ref in the Dependency Array. Since updating ref does not trigger a re-render, React cannot detect changes to current and recalculate effects or memoized values, which leads to behavior that is difficult to debug. Because ref is stable, the result doesn't change if includes ref in the dependencies array or not.

After React19 is introduced, useEffectEvent was designed precisely to incorporate this pattern into the official API.
If you have access to that environment, , there may be situations where a handwritten “latest ref” is no longer necessary.

Do callbacks really get updated that often?

The key point here is not so much that “the contents of the callback are rewritten,” but rather that a different function object is created with each render.

function SearchBox() {
  const [query, setQuery] = useState('')
  const [filter, setFilter] = useState('all')

  const search = useDebounce(() => {
    fetch(`/api?q=${query}&filter=${filter}`)
  }, 500)
}
Enter fullscreen mode Exit fullscreen mode

Although this arrow function appears only once in the source code, a new instance is created each time the page is rendered, and each instance encapsulates the current query and filter.
Since the query changes with every input, it effectively becomes a “different function” with every keystroke.

If you still hold the function that is generated in the first render, the function that runs is the function that watches query = "" and keeps searching blank. This is a typical bug of closure.

If you keep the initial render function as is, the function that checks the initial value query = ‘’ will be executed 500 ms later, and it will continue to search for an empty string indefinitely. This is a classic “stale closure” bug.

Well, why not just put it in the dependency array? But if you do that, the debounce will break.

return useMemo(() => debounce(callback, delay), [callback, delay])
Enter fullscreen mode Exit fullscreen mode

Since the callback changes with every render, the debounced instance itself is recreated each time, and any pending timers are discarded. As a result, the delay is constantly reset, rendering the debounce ineffective.

In other words, there are two requirements, and they don't align.

I want the instance of debounce tp be stable(I don't want to re-invent the wheel) and I want the function in the instance to be up-to-date.

The ref is a tool to separate the two. Replace only the contens while preserving the identity of the outer function.

It seems possible to wrap that in the useCallback. In this way, user has a responsibility to manage array dependencies perfectly, and if even one is missed, it revertsto the same closure.

If the hook's authors handle it it using ref, it will work correctly even if users write it inline.

If you use it in the useEffect, the useEffectEvent that is introduced in React 19 solves this issue.
However, the usage for holding it outside of the effect is out of responsibility and latest ref pattern still holds good.

Top comments (0)