If you’ve used `useEffect before, you’ve probably come across a "missing dependency" error at some point. It will look something like this:
React Hook useEffect has a missing dependency: 'myFunction'. Either include it
or remove the dependency array.eslint(react-hooks/exhaustive-deps)
But if you follow the advice in the warning and add the function to the dependency list, your app suddenly starts re-rendering infinitely! What's going on?
Here’s an example taken from a project I'm working on that uses the pattern popularised by Kent C. Dodds Application State Management with Reactblog post:
export const App = () => {
const { fetchClients } = useAppContext();
useEffect(() => {
fetchClients();
}, []);
return <ClientListPage />;
};
This is as simple as it gets. All I want to do is invoke fetchClients
once on
the initial render which is why I set the dependency list to be []
.
But React complains that fetchClients
is not in the dependency
list. This initially confused me as I thought the dependency list was for
variables only, but what does it mean to put a function in there? Turns out,
it’s all down to the way
Referential Equalityworks in JavaScript. Every time the App
component is rendered a new instance
of fetchClients
is created inside useAppContext
:
export function useAppContext() {
//...
const [state, dispatch] = context;
const fetchClients = async () => {
await fetchClientsAction(dispatch);
};
//...
}
What I need to do is ensure the same instance of the function is returned each time this code is invoked. Thankfully, React comes with a built-in hook that does exactly that:
const fetchClients = React.useCallback(async () => {
await fetchClientsAction(dispatch);
}, [dispatch]);
This is saying to React: _“always give me the same function instance unless
the contents of the dependency list changes”._ The dispatch
function is
guaranteed by React to be stable which is just a fancy way of saying
useReducer
will always return the same instance of the function.
So now - back in the App
component - I can safely add fetchClients
to the
dependency list:
useEffect(() => {
fetchClients();
}, [fetchClient]);
This works because the identity of fetchClients
doesn't change thanks to
useCallback
. The same instance of the function is always being returned by
useAppContext
, so it's the equivalent of invoking fetchClients
once when
the component mounts, which is exactly what I was expecting it to do in the
first place!