Tom MacWright

2025@macwright.com

The unspoken rules of React hooks

Updated to include links to documentation that does exist on the react.dev site. I do think that it's hard to discover and this concept is underemphasized relative to how important it is for working with React hooks, but as the team correctly points out, there are some docs for it.

React hooks… I have mixed feelings about them. But they are a part of the system I work with, so here are some additional notes that it's weird that some of the React docs don't cover. Mostly this is about useEffect.

The "rule of useEffect dependencies" is, in shorthand, that the dependency array should contain all of the variables referenced within the callback. So if you have a useEffect like

const [x, setX] = useState(0);
useEffect(() => {
	console.log(x);
}, []);

It is bad and wrong! You are referencing x, and you need to include it in the deps array:

const [x, setX] = useState(0);
useEffect(() => {
	console.log(x);
}, [x]);

But, this is not a universal rule. Some values are "known to be stable" and don't need to be included in your dependencies array. You can see some of these in the eslint rule implementation:

const [state, setState] = useState() / React.useState()
              // ^^^ true for this reference
const [state, dispatch] = useReducer() / React.useReducer()
              // ^^^ true for this reference
const [state, dispatch] = useActionState() / React.useActionState()
              // ^^^ true for this reference
const ref = useRef()
      // ^^^ true for this reference
const onStuff = useEffectEvent(() => {})
      // ^^^ true for this reference
False for everything else.

So, state setters, reducer dispatchers, action state dispatchers, refs, and the return value of useEffectEvent: these are all things you shouldn't put in dependencies arrays, because they have stable values. Plus the startTransition method you get out of a useTransition hook - that's also stable, just not included in that source comment.

Honestly, this is one of the things that annoys me most about hooks in React, which I touched on in my note about Remix: the useEffect and useMemo hooks rely heavily on the idea of object identity and stability. The difference between a function that changes per-render versus one whose identity stays the same is vast: if you plug an ever-changing method into useEffect dependencies, the effect runs every render. But React's documentation is underwhelming when it comes to documenting this - it isn't mentioned in the docs for useEffect, useState, or any of the other hook API pages.

As Ricky Hanlon notes, there is a note about the set method from useState being stable in the 'Synchronizing with Effects' documentation, and a dedicated guide to useEffect behavior. This is partly a discovery problem: I, and I think others, expected the stability (or instability) of return values of built-in hooks to be something documented alongside the hooks in their API docs, instead of in a topical guide.

And what about third-party hooks? I use and enjoy Jotai, which provides a hook that looks a lot like useState called useAtom. Is the state-setter I get from Jotai stable, like the one I get from useState? I think so, but I'm not sure. What about the return values of useMutation in tanstack-query, another great library. That is analogous to the setter function and, maybe it's stable? I don't know!

It's both a pretty critical part of understanding React's "dependencies" system, but it's hard to know what's stable and what's not. On the bright side, if you include some stable value in a dependencies array, it's fine - it'll stay the same and it won't trigger the effect to run or memo to recompute. But I wish that the stability or instability of the return values of hooks was more clearly part of the API guarantee of both React and third-party hook frameworks, and we had better tools for understanding what will, or won't change.