I have updated this with an update at the bottom
Is there a way to maintain a monolithic root state (like Redux) with multiple Context API Consumers working on their own part of their Provider value without triggering a re-render on every isolated change?
Having already read through this related question and tried some variations to test out some of the insights provided there, I am still confused about how to avoid re-renders.
Complete code is below and online here: https://codesandbox.io/s/504qzw02nl
The issue is that according to devtools, every component sees an "update" (a re-render), even though SectionB
is the only component that sees any render changes and even though b
is the only part of the state tree that changes. I've tried this with functional components and with PureComponent
and see the same render thrashing.
Because nothing is being passed as props (at the component level) I can't see how to detect or prevent this. In this case, I am passing the entire app state into the provider, but I've also tried passing in fragments of the state tree and see the same problem. Clearly, I am doing something very wrong.
import React, { Component, createContext } from 'react'; const defaultState = { a: { x: 1, y: 2, z: 3 }, b: { x: 4, y: 5, z: 6 }, incrementBX: () => { } }; let Context = createContext(defaultState); class App extends Component { constructor(...args) { super(...args); this.state = { ...defaultState, incrementBX: this.incrementBX.bind(this) } } incrementBX() { let { b } = this.state; let newB = { ...b, x: b.x + 1 }; this.setState({ b: newB }); } render() { return ( <Context.Provider value={this.state}> <SectionA /> <SectionB /> <SectionC /> </Context.Provider> ); } } export default App; class SectionA extends Component { render() { return (<Context.Consumer>{ ({ a }) => <div>{a.x}</div> }</Context.Consumer>); } } class SectionB extends Component { render() { return (<Context.Consumer>{ ({ b }) => <div>{b.x}</div> }</Context.Consumer>); } } class SectionC extends Component { render() { return (<Context.Consumer>{ ({ incrementBX }) => <button onClick={incrementBX}>Increment a x</button> }</Context.Consumer>); } }
Edit: I understand that there may be a bug in the way react-devtools detects or displays re-renders. I've expanded on my code above in a way that displays the problem. I now cannot tell if what I am doing is actually causing re-renders or not. Based on what I've read from Dan Abramov, I think I'm using Provider and Consumer correctly, but I cannot definitively tell if that's true. I welcome any insights.
✅ Preventing Context re-renders: Context selectorsThere is no way to prevent a component that uses a portion of Context value from re-rendering, even if the used piece of data hasn't changed, even with useMemo hook. Context selectors, however, could be faked with the use of higher-order components and React. memo .
This is the core principal of Context API, when a context value changed all components re-render. In order to prevent this we can use memo which will skip unnecessary re-renders of that component. import { memo } from "react"; const MidChild = memo(() => { console.
Context and React renderingWhen a component renders, React will recursively re-render all its children regardless of props or context.
There are some ways to avoid re-renders, also make your state management "redux-like". I will show you how I've been doing, it far from being a redux, because redux offer so many functionalities that aren't so trivial to implement, like the ability to dispatch actions to any reducer from any actions or the combineReducers and so many others.
export const initialState = { ... }; export const reducer = (state, action) => { ... };
export const AppContext = React.createContext({someDefaultValue}) export function ContextProvider(props) { const [state, dispatch] = useReducer(reducer, initialState) const context = { someValue: state.someValue, someOtherValue: state.someOtherValue, setSomeValue: input => dispatch('something'), } return ( <AppContext.Provider value={context}> {props.children} </AppContext.Provider> ); }
function App(props) { ... return( <AppContext> ... </AppContext> ) }
This way they will only re-render when those specific dependencies update with new values
const MyComponent = React.memo(({ somePropFromContext, setSomePropFromContext, otherPropFromContext, someRegularPropNotFromContext, }) => { ... // regular component logic return( ... // regular component return ) });
function select(){ const { someValue, otherValue, setSomeValue } = useContext(AppContext); return { somePropFromContext: someValue, setSomePropFromContext: setSomeValue, otherPropFromContext: otherValue, } }
function connectToContext(WrappedComponent, select){ return function(props){ const selectors = select(); return <WrappedComponent {...selectors} {...props}/> } }
import connectToContext from ... import AppContext from ... const MyComponent = React.memo(... ... ) function select(){ ... } export default connectToContext(MyComponent, select)
<MyComponent someRegularPropNotFromContext={something} /> //inside MyComponent: ... <button onClick={input => setSomeValueFromContext(input)}>... ...
Demo on codesandbox
MyComponent
will re-render only if the specifics props from context updates with a new value, else it will stay there. The code inside select
will run every time any value from context updates, but it does nothing and is cheap.
I suggest check this out Preventing rerenders with React.memo and useContext hook.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With