OMGItsRob
OMGItsRob

Reputation: 125

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

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.

      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);
  return (
    <div>
      <About arrayProp={state} />
    </div>
  );
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.

Upvotes: 0

Views: 2125

Answers (1)

Henry Ecker
Henry Ecker

Reputation: 35626

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>

Upvotes: 1

Related Questions