Hooks that wrap unstable values for more control over incoming hook dependencies
@ricokahler/stable-hooks
⚠️ This library is well-tested but the README/docs still needs work. A 1.0 should be around the corner.
hooks that wrap unstable values for more control over incoming hook dependencies
Installation
npm i --save @ricokahler/stable-hooks
Motivation
In complex React components, it quickly becomes challenging to control how incoming values affect your downstream hooks.
For example, the following <Dialog />
component has a bug that causes it to re-run the onOpen
or onClose
callbacks if the consumer does not wrap the callbacks in useCallback
.
import { useState, useEffect } from 'react';
function Dialog({ onOpen, onClose }) {
const [open, setOpen] = useState(false);
useEffect(() => {
if (open) onOpen();
else onClose();
}, [open, onOpen, onClose]);
return (
<>
<button onClick={() => setOpen(!open)}>Toggle Dialog</button>
<dialog open={open}>
<p>Greetings, one and all!</p>
</dialog>
</>
);
}
export default function App() {
const [clicks, setClicks] = useState(0);
return (
<>
<button onClick={() => setClicks(clicks + 1)}>Clicks {clicks}</button>
<Dialog
onOpen={() => console.log('Dialog was opened!')}
onClose={() => console.log('Dialog was closed!')}
/>
</>
);
}
stable-hooks
are hooks you can use to wrap unstable values for more control over incoming hook dependencies.
Usage
useStableGetter
Wraps incoming values in a stable getter function that returns the latest value.
Useful tool for signifying a value should not be considered as a reactive dependency.
ℹ When this getter is invoked, it pulls the latest value from a hidden ref. This ref is synced with the current inside of a
useLayoutEffect
so that it runs before otheruseEffect
s.
import { useState, useEffect } from 'react';
import { useStableGetter } from '@ricokahler/stable-hooks';
function Dialog(props) {
const [open, setOpen] = useState(false);
const getOnOpen = useStableGetter(props.onOpen);
const getOnClose = useStableGetter(props.onClose);
useEffect(() => {
const onOpen = getOnOpen();
const onClose = getOnClose();
if (open) onOpen();
else onClose();
}, [open, getOnOpen, getOnClose]);
return (
<>
<button onClick={() => setOpen(!open)}>Toggle Dialog</button>
<dialog open={open}>
<p>Greetings, one and all!</p>
</dialog>
</>
);
}
useStableCallback
Returns a stable callback that does not change between re-renders.
ℹ The implementation uses
useStableGetter
to get latest version of the callback (and the values closed within it) so values are not stale between different invocations.
import { useState, useEffect } from 'react';
import { useStableCallback } from '@ricokahler/stable-hooks';
function Dialog(props) {
const [open, setOpen] = useState(false);
const onOpen = useStableCallback(props.onOpen);
const onClose = useStableCallback(props.onClose);
useEffect(() => {
if (open) onOpen();
else onClose();
}, [open, onOpen, onClose]);
return (
<>
<button onClick={() => setOpen(!open)}>Toggle Dialog</button>
<dialog open={open}>
<p>Greetings, one and all!</p>
</dialog>
</>
);
}
useStableValue
Given an unstable value, useStableValue
hashes the incoming value against a hashFn
(by default, this is JSON.stringify
) and if the hash is unchanged, the previous value will be returned.
Useful for defensively programming against unstable objects coming from props.
ℹ The implementation runs the value through the provided hash function and the result of that hash function is used as the only dependency in a
useMemo
call. See the implementation here.
function Example(props) {
const style = useStableValue(props.style);
useEffect(() => {
// do something only when the _contents_ of
// the style object changes
}, [style]);
return <>{/* ... */}</>;
}