Suppose I implement a simple global loading state like this:
// hooks/useLoading.js
import React, { createContext, useContext, useReducer } from 'react';
const Context = createContext();
const { Provider } = Context;
const initialState = {
isLoading: false,
};
function reducer(state, action) {
switch (action.type) {
case 'SET_LOADING_ON': {
return {
...state,
isLoading: true,
};
}
case 'SET_LOADING_OFF': {
return {
...state,
isLoading: false,
};
}
}
}
export const actionCreators = {
setLoadingOn: () => ({
type: 'SET_LOADING_ON',
}),
setLoadingOff: () => ({
type: 'SET_LOADING_OFF',
}),
};
export const LoadingProvider = ({ children }) => {
const [{ isLoading }, dispatch] = useReducer(reducer, initialState);
return <Provider value={{ isLoading, dispatch }}>{children}</Provider>;
};
export default () => useContext(Context);
Then suppose I have a component that mutates the loading state, but never consumes it, like this:
import React from 'react';
import useLoading, { actionCreators } from 'hooks/useLoading';
export default () => {
const { dispatch } = useLoading();
dispatch(actionCreators.setLoadingOn();
doSomethingAsync().then(() => dispatch(actionCreators.setLoadingOff()))
return <React.Fragment />;
};
According to useReducer docs, dispatch is has a stable identity. I interpreted this to mean that when a component extracts dispatch from a useReducer, it won't re-render when the state connected to that dispatch changes, because the reference to dispatch will always be the same. Basically, dispatch can "treated like a static value".
Yet when this code runs, the line dispatch(actionCreators.setLoadingOn())
triggers an update to global state and the useLoading
hook is ran again and so is dispatch(actionCreators.setLoadingOn())
(infinite re-renders -_-)
Am I not understanding useReducer correctly? Or is there something else I'm doing that might be causing the infinite re-renders?
doSomethingAsync
might be the problem because it is rerunning on every render. In most cases, you'd want to wrapdoSomethingAsync
with auseEffect(() => {...}, [])
to prevent it from rerunning on every render. Same goes fordispatch(actionCreators.setLoadingOn());
. If it isn't wrapped in a useEffect, it's going to dispatchsetLoadingOn
on every render which will cause a rerender. Does this pseduocode correctly match your actual issue, or should this be updated to better match reality with moreuseEffect
s? – DrucillasetLoadingOn();
does not close a paren. – KimonClick={() => dispatch(actionCreators.setLoadingOn())}
Details aside, at high level, what we would have is a pure functional component that mutates some state. But according to the rules of hooks, a component like this would re-render on every state change even though it doesn't subscribe to any of the state it mutates. Of course I could use something likeuseMemo
to control this components re-rendering rules, but still. It just seems odd – Koehler