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?
The first issue is that you should never trigger any React state updates while rendering, including useReducers
's dispatch()
and useState
's setters.
The second issue is that yes, dispatching while always cause React to queue a state update and try calling the reducer, and if the reducer returns a new value, React will continue re-rendering. Doesn't matter what component you've dispatched from - causing state updates and re-rendering is the point of useReducer
in the first place.
The "stable identity" means that the dispatch
variable will point to the same function reference across renders.
Definitely, this was more for demo purposes. Perhaps a more realistic a more realistic example would be if this were a button. Maybe something like: onClick={() => dispatch(actionCreators.setLoadingOn())} Details aside, at a 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 like useMemo to control this components re-rendering rules, but still. It just seems odd
Not sure what you're pointing to as a problem here. Remember that React's default behavior is to always recursively re-render on every state change. There's nothing special about
useReducer
in that regard - it's just a different mechanism for organizing the state update logic in a component.Yeah I see that now. Thanks. Now I'm curious what advantages
useReducer
has overuseState
. When I would implement global state just usinguseState
andContext
, I would pass getter and setter callbacks through the global state hook. I thought this was bad practice for components that only use setter callbacks since it would cause needless re-renders. I thought the "identity safe" dispatch would solve this problem, but it doesn't haha.The stable identity of
dispatch
and setters is useful if the child components are attempting to optimize re-renders via prop comparisons (ie,React.memo()
andPureComponent
). Overall,useReducer
is helpful if you have complex state update logic, need to avoid closures that must read the current state to calculate a new one, or want to allow child components to just indicate "some event happened" and keep the logic higher up.