skip to Main Content

Goal: one child should register the update from another child, then not re-render infinitely. The following is a story of no updates or infinite loops. If you are a master of hooks (or at least not an amateur like I), this is for you.

Suppose there is a single context that will hold state.

const TestContext = React.createContext('hello')

Suppose that in the main App.js file, there is a single state variable holding a complex, nested object.

const [testState,setTestState] = React.setState({})

There are multiple children that will receive the state through a Provider:

<TestContext.Provider value={[testState,setTestState]}>

{SomeChildrenGoHere}

</TestContext.Provider>

Now suppose one of those children wishes to update the context on load:

var TestComponentOne = function(){
    
    const [testState,setTestState] = React.useContext(TestContext)
    
    var GetTestVar(){
    
    if('testVar2' in testState){
    
        return(<h1>Success</h1>)
    
    }
    
    else{
    
        return(<h1>Not yet...</h1>)
    
    }
    
    var UpdateState = function(){
    
       setTestState({...testState,"testVar1":"testVar1"})
    
    }
    
    React.useEffect(()=>{
       UpdateState()
    
    },[testState])
    
}

And another child component wants to do the same…

var TestComponentTwo = function(){
    
    const [testState,setTestState] = React.useContext(TestContext)
    
    var GetTestVar(){
    
    if('testVar1' in testState){
    
        return(<h1>Success</h1>)
    
    }
    
    else{
    
        return(<h1>Not yet...</h1>)
    
    }
    
    var UpdateState = function(){
    
        setTestState({...testState,"testVar2":"testVar2"})
    
    }
    
    React.useEffect(()=>{
       UpdateState()
    
    },[testState])
    
}

I have tried the following useEffects however I either get zero updates (as expected with an empty array) or an infinite loop.

Permutation 1:

React.useEffect(()=>{
           UpdateState()
        
        },[testState])

Permutation 2 (No reload expected):

React.useEffect(()=>{
           UpdateState()
        
        },[])

Permutation 3:

React.useEffect(()=>{
           UpdateState()
        
        })

The question is: how to achieve the functionality without the infinite re-renders or lack of rendering past the initial load? The context state should be updateable from children while listening for updates from other children. Dependencies should not be hard coded other than the state object. In other words, no specific keys as dependencies.

Also should note, I’ve tried wrapping the useContext in a useRef.

Edit: https://playcode.io/1241366

2

Answers


  1. You are having state race condition between the 2 components. So you need to wait for one component to change the state and then change the state in the other component before the other component.

    To solve it, you need to create a new state to check if the state was already updated.

    I edited the code you shared in the comments.

    import React from 'react';
    
    const TestContext = React.createContext('hello');
    
    var TestComponentOne = function () {
      const { testData, setTestData, isRendered } = React.useContext(TestContext);
    
      var SetState = function () {
        setTestData({
          ...testData,
    
          testOne: 'Hello from Test One',
        });
      };
    
      React.useEffect(() => {
        isRendered && SetState();
    
        console.log(testData);
      }, [isRendered]);
    
      var GetState = function () {
        if (testData && 'testTwo' in testData) {
          return testData['testTwo'];
        } else {
          return 'No luck in getting the updated state from Component Two';
        }
      };
    
      return <p>{GetState()}</p>;
    };
    
    var TestComponentTwo = function () {
      const { testData, setTestData, setIsRendered } = React.useContext(TestContext);
    
      var SetState = function () {
        setTestData({
          ...testData,
    
          testTwo: 'Hello from Test Two',
        });
      };
    
      React.useEffect(() => {
        SetState();
        setIsRendered(true);
        console.log(testData);
      }, []);
    
      var GetState = function (number) {
        if (testData && 'testOne' in testData) {
          return testData['testOne'];
        } else {
          return 'No luck in getting the updated state from Component One';
        }
      };
    
      return <p>{GetState()}</p>;
    };
    
    export function App(props) {
      const [testData, setTestData] = React.useState({});
      const [isRendered, setIsRendered] = React.useState(false);
    
      return (
        <TestContext.Provider value={{ testData, setTestData, isRendered, setIsRendered }}>
          <div className='App'>
            <h1>Hello React.</h1>
            <h2>Start editing to see some magic happen!</h2>
            <p></p>
            <TestComponentOne />
            <TestComponentTwo />
          </div>
        </TestContext.Provider>
      );
    }
    
    // Log to console
    console.log('Hello console');
    

    EDIT: please check @damonholden for the correct answer using callback when setting the state.

    Login or Signup to reply.
  2. After better understanding your issue, your problem is not to do with context or your component effects at all. You are just simply updating state multiple times in a single render cycle in a way that results in React not storing each change.

    Consider the following functions from your component tree:

    //...
    
    var SetState = function () {
        setTestData({
            ...testData,
    
            testOne: 'Hello from Test One',
        });
    };
    
    //...
    
    var SetState = function () {
        setTestData({
            ...testData,
    
            testTwo: 'Hello from Test Two',
        });
    };
    
    //...
    

    When your app renders and your component effects are executed, React will process the previous two functions in the following way:

    • The first setState function is invoked – asking React to que a re-render and setting the testData state from {} to {testOne: 'Hello from Test One'} after the re-render.
    • Later on, the second setState function is invoked, which you may think changes the same testData state to {testOne: 'Hello from Test One', testTwo: 'Hello from Test Two'}. However, React has not triggered a re-render yet and therefore testData still references {}, so now, after a re-render, testData will become {testTwo: 'Hello from Test Two'}.

    This is why it almost seems like there is some miscommunication between your state updates, but this is expected behaviour in React. The solution is to use callbacks in your state setters so that React can use the result of each setter callback as the data for the next:

    var SetState = function () {
        setTestData((previousState) => {
            return {
                ...previousState,
                testOne: 'Hello from Test One',
            };
        });
    };
    
    var SetState = function () {
        setTestData((previousState) => {
            return {
                ...previousState,
                testTwo: 'Hello from Test Two',
            };
        });
    };
    

    This behaviour of setting state in React is well documented and you can read more about it on their official documentation, here.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search