Home > Mobile >  React async/await prop not re-rendering in child component
React async/await prop not re-rendering in child component

Time:07-23

Link to CodeSandBox of what I am experiencing:

https://codesandbox.io/s/intelligent-chaum-eu1le6?file=/src/About.js

I am stuggling to figure out why a component will not re-render after a state changes. In this example, it is an array prop given from App.js to About.js.

  • a fetch request happens three times in a useEffect. Each time, it pushes it to stateArr before finally setState(stateArr)
      fetch("https://catfact.ninja/fact")
        .then((res) => {
          return res.json();
        })
        .then((res) => {
          stateArr.push(res);
        });
      fetch("https://catfact.ninja/fact")
        .then((res) => {
          return res.json();
        })
        .then((res) => {
          stateArr.push(res);
        });
      fetch("https://catfact.ninja/fact")
        .then((res) => {
          return res.json();
        })
        .then((res) => {
          stateArr.push(res);
        });
      setState(stateArr);
  • The About component is imported, and the useState variable is passed to it as a prop.
  return (
    <div>
      <About arrayProp={state} />
    </div>
  );
  • Finally, About.js destructs the prop, and arrayProp.map() is called to render each array item on the page.
const About = ({ arrayProp }) => {
  const [rerender, setRerender] = useState(0);

  return (
    <>
      {arrayProp.map((e) => (
        <div key={e.length}>
          <h6>Break</h6>
          {e.fact}
        </div>
      ))}
    </>
  );
};

In the CodeSandBox example, I've added a button that would manually re-render the page by incrementing a number on the page. The prop should prompt a component re-render after the fetch requests are completed, and the state is changed.

CodePudding user response:

The issue is that useEffect is not behaving as described.

Each time, it pushes it to stateArr before finally setState(stateArr)

The individual fetches are not pushing to "before finally" calling setState.

const [state, setState] = useState([]);
useEffect(() => {
    let stateArr = [];

    function getReq() {
        fetch("https://catfact.ninja/fact")
            .then((res) => {
                return res.json();
            })
            .then((res) => {
                stateArr.push(res);
            });
        fetch("https://catfact.ninja/fact")
            .then((res) => {
                return res.json();
            })
            .then((res) => {
                stateArr.push(res);
            });
        fetch("https://catfact.ninja/fact")
            .then((res) => {
                return res.json();
            })
            .then((res) => {
                stateArr.push(res);
            });
        setState(stateArr);
    }

    getReq();
}, []);

What is actually happening is: fetch 1 is starting, then fetch 2 is starting, then fetch 3 is starting, then setState(stateArr) is being called.

There's no guarantee that these fetch will resolve before setState is called (there's similarly no guarantee that the fetches won't complete before calling setState). Though, in normal circumstances none of the fetches will resolve before setState is called.

So the only thing that's guaranteed is that state will be updated to reference the same array as stateArr. For this reason, pushing to stateArr is the same as pushing to state which is mutating state without using setState. This can cause results to be overwritten on future setState calls and it does not cause a re-render.


Well then, why does forcing re-render in About work?

As each fetch resolves it pushes values to stateArr (which is the same array as is referenced by state) for this reason the values are in the state there's just been nothing to tell React re-render (like a setState call).

Here's a small snippet which logs the promises as they complete. It also has a button that will console log the state array. (Nothing will ever render here as nothing will cause the state to update despite the state array being modified)

// Use import in normal cases; const is how use* are accessed in Stack Snippets
const {useState, useEffect} = React;

const App = () => {
    const [state, setState] = useState([]);
    useEffect(() => {
        let stateArr = [];

        function getReq() {
            fetch("https://catfact.ninja/fact")
                .then((res) => {
                    return res.json();
                })
                .then((res) => {
                    stateArr.push(res);
                    console.log('Promise 1 resolves', stateArr);
                });
            fetch("https://catfact.ninja/fact")
                .then((res) => {
                    return res.json();
                })
                .then((res) => {
                    stateArr.push(res);
                    console.log('Promise 2 resolves', stateArr);
                });
            fetch("https://catfact.ninja/fact")
                .then((res) => {
                    return res.json();
                })
                .then((res) => {
                    stateArr.push(res);
                    console.log('Promise 3 resolves', stateArr);
                });
            console.log('Calling Set State')
            setState(stateArr);
        }

        getReq();
    }, []);

    return (
        <div>
            <button onClick={() => console.log(state)}>Log State Array</button>
            {state.map((e) => (
                <div key={e.length}>
                    <h6>Break</h6>
                    {e.fact}
                </div>
            ))}
        </div>
    );
}


ReactDOM.createRoot(
    document.getElementById("root")
).render(
    <App/>
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
<div id="root"></div>


To resolve this, simply wait for all promises to complete with Promise.all, then call setState with all the values.

const [state, setState] = useState([]);
useEffect(() => {
    Promise.all([
        // Promise 1
        fetch("https://catfact.ninja/fact").then((res) => {
            return res.json();
        }),
        // Promise 2
        fetch("https://catfact.ninja/fact").then((res) => {
            return res.json();
        }),
        // Promise 3
        fetch("https://catfact.ninja/fact").then((res) => {
            return res.json();
        })
    ]).then((newStateArr) => {
        // Wait for all promises to resolve before calling setState
        setState(newStateArr);
    });
}, []);

And here's a snippet demoing the result when waiting for all promises to resolve:

// Use import in normal cases; const is how use* are accessed in Stack Snippets
const {useState, useEffect} = React;

const App = () => {
    const [state, setState] = useState([]);
    useEffect(() => {
        Promise.all([
            // Promise 1
            fetch("https://catfact.ninja/fact").then((res) => {
                return res.json();
            }),
            // Promise 2
            fetch("https://catfact.ninja/fact").then((res) => {
                return res.json();
            }),
            // Promise 3
            fetch("https://catfact.ninja/fact").then((res) => {
                return res.json();
            })
        ]).then((newStateArr) => {
            // Wait for all promises to resolve before calling setState
            setState(newStateArr);
        });
    }, []);

    return (
        <div>
            {state.map((e) => (
                <div key={e.length}>
                    <h6>Break</h6>
                    {e.fact}
                </div>
            ))}
        </div>
    );
}


ReactDOM.createRoot(
    document.getElementById("root")
).render(
    <App/>
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
<div id="root"></div>

  • Related