Simple React hooks to access referentially stable, up-to-date versions of non-primitives.
import { useStableCallback, useStableValue } from "use-stable-reference";
function Library({ unstableCallback, unstableValue }) {
const stableCallback = useStableCallback(unstableCallback);
const getStableValue = useStableValue(unstableValue);
useEffect(() => {
if (/* ... */) {
stableCallback()
const stableValue = getStableValue()
}
// safe to add to dependency arrays!
}, [stableCallback, getStableValue, /* ... */]);
}
use-stable-reference
really shines for library authors or for those writing reusable code. With a library-consumer relationship, the library author can't reasonably expect that the consumer will preemptively wrap any callbacks in a useCallback
, or any referentially unstable values in a useMemo
. This leaves the author with a few possible choices for how to handle consumer-provided non-primitive arguments:
- Leave them out of any dependency arrays, and ignore any eslint React linter warnings/errors
- Leave them in the dependency arrays, expecting that the effects / memoizations will run every render
- Wrap them in a
useStableCallback
/useStableValue
With option 3, the returned callback/value-getter are referentially stable, can safely be used in dependency arrays, and are guaranteed to always be up-to-date if the underlying option ever changes! 🎉
useStableCallback
accepts one argument, a callback of type: (...args: any[]) => any
useStableCallback
returns an up-to-date, referentially stable callback.
useStableValue
accepts one argument, a value of type: unknown
useStableValue
returns a referentially stable callback that returns an up-to-date copy of the argument.
A version of this hook has been floating around the React community for a while, often referred to as useEvent
or useEffectCallback
. This package hopes to distill the best aspects of several different implementations:
-
useEvent
RFC, legacy React docs- Basic implementation
-
wouter
- Initializing the
useRef
to the callback argument, rather thannull
- Initializing the
-
react-use-event-hook
- Support passing a callback argument that uses the
this
keyword
- Support passing a callback argument that uses the
-
react-use/useLatest
- Updating the
ref
in the render method - This method is controversial, but I think the trade-offs are worth it; see below.
- Updating the
Updating a ref
in the render method is only dangerous when using concurrent features. Consider the following scenario:
- A component re-renders, i.e. the render method runs
- The
ref
is updated - The DOM updates are discarded because a second, higher-priority render was triggered
- The higher-priority render occurs
- Any code which uses the
ref
value before it's updated in step6
is using a value from a render that was discarded! - The
ref
is updated to the intended value
Thankfully, this is rarely something we need to worry about, for a few reasons:
- Concurrent mode is opt-in, triggered only when using concurrent features
- Concurrent features are only available in React 18+
- The React compiler, which will make this library unnecessary, is in beta starting with React 19
- The callbacks and values that are passed to
useStableCallback
anduseStableValue
may be referentially unstable, but generally have the same behavior from render to render
In other words, for developers using React < 18, there's no issue because concurrent features aren't available; for devs using React > 19, you shouldn't need this package at all because of the React compiler; for those stuck in the middle using React 18, there's a good chance all the ref
values will have the same behavior anyway, as long as you pass a callback/value with the same behavior every render.
That leaves just one scenario to consider. For devs using React 18, with concurrent features, with dynamic callbacks/values, consider yourselves warned: your refs may be out-of-sync with your render!