Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React Context API and avoiding re-renders

Tags:

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.

like image 368
Andrew Avatar asked Jul 13 '18 03:07

Andrew


People also ask

How do I prevent context from Rerendering?

✅ 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 .

How can I avoid component re use problem with context API?

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.

Does React context Rerender everything?

Context and React renderingWhen a component renders, React will recursively re-render all its children regardless of props or context.


1 Answers

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.

Create your reducer

export const initialState = {   ... };  export const reducer = (state, action) => {   ... }; 

Create your ContextProvider component

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>   ); } 

Use your ContextProvider at top level of your App, or where you want it

function App(props) {   ...   return(     <AppContext>       ...     </AppContext>   ) } 

Write components as pure functional component

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     ) }); 

Have a function to select props from context (like redux map...)

function select(){   const { someValue, otherValue, setSomeValue } = useContext(AppContext);   return {     somePropFromContext: someValue,     setSomePropFromContext: setSomeValue,     otherPropFromContext: otherValue,   } } 

Write a connectToContext HOC

function connectToContext(WrappedComponent, select){   return function(props){     const selectors = select();     return <WrappedComponent {...selectors} {...props}/>   } } 

Put it all together

import connectToContext from ... import AppContext from ...  const MyComponent = React.memo(...   ... )  function select(){   ... }  export default connectToContext(MyComponent, select) 

Usage

<MyComponent someRegularPropNotFromContext={something} />  //inside MyComponent: ...   <button onClick={input => setSomeValueFromContext(input)}>... ... 

Demo that I did on other StackOverflow question

Demo on codesandbox

The re-render avoided

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.

Other solutions

I suggest check this out Preventing rerenders with React.memo and useContext hook.

like image 70
pedrobern Avatar answered Sep 27 '22 18:09

pedrobern