When we use useContext for data flow management, every time the context is updated, all components that use the context are re-rendered. If the context’s data is made up of multiple parts, and only one or two of the fields are updated frequently, but the rest of the data is stable, then the component’s value will render frequently even if it uses the stable part of the data, which can easily cause performance problems. We usually use split context or useMemo to reduce the number of component renderings:
1. Split the context
We can do this by splitting the context into instableContext, which holds unstable data, and stableContext, which holds stable data.
const InstableStateContext = React.createContext();
const StableStateContext = React.createContext();
function Provider({children}) {
const [instableState, instableDispatch] = React.useState();
const [stableState, stableDispatch] = React.useState();
return (
<StableStateContext.Provider value={{state:stableState, dispatch:stableDispatch}} >
<InstableStateContext.Provider value={{state:instableState, dispatch:instableDispatch}} >
{children}
</InstableStateContext.Provider>
</StableStateContext.Provider>)}Copy the code
For components that only use stable data, we just use stableContext,
//stableComponent.js function stableComponent() { const {state} = React.useContext(StableStateContext); return ... ; }Copy the code
This allows stablecomponent.js to trigger rendering only when the data InstableStateContext is updated, regardless of InstableStateContext
2. Wrap functions with useMemo
UseMemo can pass in a data generator and dependencies, which can cause the data generator to recalculate the value of the data to be generated if and only if the dependency changes. We can wrap the return value of the component in useMemo, passing in the data to be used as a dependency
const {state}= useContext(AppContext);
return useMemo(() => <span>data:{state.depData}</span>, [state.depData]);
Copy the code
In the example above, the component is rerendered if and only if depData changes.
Both of these methods can reduce unnecessary rendering, but they always feel unelegant (and cumbersome) to write. Let’s look at another way to reduce unnecessary rendering using useContext.
Use publish subscriptions to reduce unnecessary rendering caused by using useContext
Is there a way to trigger rendering only when the context data we are using changes, without the tedious wrapping of useMemo? We can create a store that has a getState method that we can use to get the data that’s stored in the context.
const [state, dispatch] = useReducer(this.reducer, initState);
const store = {
getState: () = > state,
dispatch,
}
Copy the code
We wrap the store values with useMemo and deps is an empty array:
const [state, dispatch] = useReducer(this.reducer, initState);
const stateRef = useRef(state);
stateRef.current = state;
const store =useMemo(() = > ({
getState: () = > stateRef.current,
dispatch,
}),[]);
Copy the code
This does not change the reference to the store value. If we pass store as the context.Provider value:
Provider = (props: ProviderProps) => { const { children, initState = {} } = props; const [state, dispatch] = useReducer(this.reducer, initState); const stateRef = useRef(state); stateRef.current = state; Const store = useMemo(() => ({getState: () => stateref.current, dispatch,}), [],); return <this.context.Provider value={store}>{children}</this.context.Provider>; };Copy the code
This way, the component under the Provider does not trigger rendering because of state changes. However, since the store value does not change, the component within the provider has no way of knowing when to render. At this point we introduce a publish-subscribe pattern to inform the component when to render. When state changes, we fire the stageChange event:
Provider = (props: ProviderProps) => { const { children, initState = {} } = props; const [state, dispatch] = useReducer(this.reducer, initState); const stateRef = useRef(state); stateRef.current = state; UseEffect (() => {// Tell useSelector that state has been updated and let it emit forceUpdate this.emit('stateChange'); }, [state]); Const store = useMemo(() => ({getState: () => stateref.current, dispatch,}), [],); return <this.context.Provider value={store}>{children}</this.context.Provider>; };Copy the code
UseSelector, described below, subscribs to this event to tell the component that it needs to be rerendered. Next we’ll implement a useSelector method that acts as a bridge to get the data in state within the component. It takes a selector function as an argument, such as:
const a = useSelector(state=>state.a)
Copy the code
So, we can get a in state. The next thing we need to do is make sure that when state.a is updated, the component can trigger a render and get the latest A. In useSelector, we’re going to subscribe to the stageChange event, and then we’re going to check if the selector has changed, and if it has, we’re going to use forceUpdate to force rendering;
useSelector: UseSelector = (selector) => { const forceUpdate = useForceUpdate(); const store = useContext<any>(this.context); const latestSelector = useRef(selector); const latestSelectedState = useRef(selector(store.getState())); if (! Store) {throw new Error(' You must use useSelector within the Provider '); } latestSelector.current = selector; latestSelectedState.current = selector(store.getState()); useEffect(() => { const checkForUpdates = () => { const newSelectedState = latestSelector.current(store.getState()); // When state changes, check whether the current selectedState is the same as the updated selectedState. If the current selectedState is inconsistent, render if (! isEqual(newSelectedState, latestSelectedState.current)) { forceUpdate(); }}; this.on('stateChange', checkForUpdates); return () => { this.off('stateChange', checkForUpdates); }; }, [store]); return latestSelectedState.current; };Copy the code
ForceUpdate is also very simple: it triggers component updates by changing an unwanted state:
const useForceUpdate = () => { const [_, setState] = useState(false); return () => setState((val) => ! val); };Copy the code
So, when we get the data when we use useSelector on the component, the component is only re-rendered when the selector is updated.