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!
