useEffectEvent

While reading some release notes for eslint-plugin-react-hooks1, something caught my eye: useEffectEvent.
I’ve never seen that hook before, and it’s definitely not part of React 18. Let’s look at the docs and see what it is.
Separating events from effects.
Ah, so it helps us use values in a useEffect without causing it to tear down and re-run again.
The docs page is well worth reading, but let’s look at some simplified examples.
useEffect is for running some code when one or more values change.
useEffect(
// Function that will run whenever anything in the deps array changes.
() => {
connect(serverUrl, roomId);
// Cleanup after a dep changes
return () => {
disconnect();
}
},
// Deps array. The function above will run whenever any these change.
// Values can come from props, state, wherever.
[serverUrl, roomId],
);
You’re supposed to list all the values the effect uses in its deps array. eslint-plugin-react-hooks enforces this.
What if you want to use a value in the effect, but not have the effect re-run when it changes?
Currently there are 2 ways to do this.
First, you can exclude the value from the deps array and ignore the lint error.
useEffect(
() => {
connect(roomId);
// We want to use the `time` value, but not re-run the effect if
// it changes.
showNotification(time);
return () => {
disconnect();
}
},
// Exclude `time`. The react-hooks lint plugin is not going to be happy...
[serverUrl, roomId],
);
This works, but it’s clearly not what you’re “supposed” to do2
Second, you can capture the value in a ref.
const timeRef = useRef(time);
// Keep the ref value updated.
useEffect(() => {
timeRef.current = time;
}, [time]);
useEffect(
() => {
connect(roomId);
// Use the ref value.
showNotification(timeRef.current);
return () => {
disconnect();
}
},
// No need to include timeRef as a dep. The ref object is "stable"
// (even though its current value changes), and the lint rule knows it.
[serverUrl, roomId],
);
This works, but can be a little annoying to write. Especially repeatedly.
useEffectEvent will let us do this in a cleaner way
You pull the “non-reactive” values into a sort-of “event”.
const onConnected = useEffectEvent(() => {
showNotification(time);
});
useEffect(
() => {
connect(roomId);
// Use the "effect event".
onConnected();
return () => {
disconnect();
}
},
// Again, no need to include the effect event in the deps array.
[serverUrl, roomId],
);
Looking at the useEffectEvent implementation, it’s actually doing something similar to our ref example.
On every render it captures the callback and puts it in an object. And then returns a function that’ll execute that callback.
Interestingly, it throws an error if called during rendering. So you can only call the function inside an effect. Which makes sense, because that’s the whole point of it.
What, you guys don’t spend your precious time reading release notes of eslint plugins?
Although I won’t tell anyone you did this 🤫.