skip to Main Content

When I run the following code, a strange issue occurs :

import {useState, useEffect} from 'react'

function App() {
  const [data, setData] = useState([]);

  useEffect(()=>{
    {console.log("useEffect start")}                     // Step 1 after useEffect start
    const getApiData = async () => {
      console.log("async function start")                // Step 2
      const apiData = await fetch("https://api.chucknorris.io/jokes/random").then(res=>res.json());
      console.log("get API data")                        // Step 3
      setData(data.concat(apiData));
      console.log("after setData",data)}                 // Step 6
    getApiData();
  },[])
  console.log("outside useEffect",data);                 // Step 4

  return (<>
     <li>{data.map(i => <li>{i.value}</li>)}</li>
     {console.log("inside return")}                      // Step 5
  </>)}

export default App

Here are the sequence results from the console:

outside useEffect []
inside return
useEffect start
async function start
get API data
outside useEffect [{…}]  // Since useState(setData) is asynchronous, 
                         // why this step can get the return data value before setData is completed?
inside return
after setData []

What I don’t understand is, if useState(setData) is asynchronous, why can I get the return value of data in Step 4? Since I can get the return value of data in Step 4, it means that useState(setData) has already completed before Step 4. If so, why does Step 6 come after Step 4? If useState(setData) has already been completed, then according to the sequence of the code lines, the next execution should be console.log("after setData", data) . Do I miss something here?

Stack Snippet:

<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>

<script type="text/babel" data-presets="es2017,react,stage-3">
const { useState, useEffect } = React;

function App() {
  const [data, setData] = useState([]);

  useEffect(()=>{
    {console.log("useEffect start")}                     // Step 1 after useEffect start
    const getApiData = async () => {
      console.log("async function start")                // Step 2
      const apiData = await fetch("https://api.chucknorris.io/jokes/random").then(res=>res.json());
      console.log("get API data")                        // Step 3
      setData(data.concat(apiData));
      console.log("after setData",data)}                 // Step 6
    getApiData();
  },[])
  console.log("outside useEffect",data);                 // Step 4

  return (<>
     <li>{data.map(i => <li>{i.value}</li>)}</li>
     {console.log("inside return")}                      {/* Step 5*/}
  </>)}

const root = ReactDOM.render(<App />, document.getElementById("root"));
</script>
<script src="https://unpkg.com/[email protected]/runtime.js"></script>
<script src="https://unpkg.com/@babel/[email protected]/babel.min.js"></script>

enter image description here

3

Answers


  1. setData update all List And then he did it again so you can again see outside useEffect [{…}]

    Login or Signup to reply.
  2. This is because React will run twice with useEffect in development mode.

    Try see result after run build.

    Or remove <React.StrictMode> component to disable the default behaviour.

    Result:

    enter image description here

    Login or Signup to reply.
  3. I will try to address directly the part which is most confusing.

    Why do we get the below output?

    get API data
    outside useEffect [{…}]  
    

    Isn’t setState asynchronous?

    setState batches state updates and delays rendering in most cases i.e., when the updates are from event handlers, but not from promises.

    Here is the github isuse:

    This is one of the useful comments:

    This is entirely standard and intentional behavior. Currently, React only batches state updates that happen within its own event handlers. Logic in a promise callback is outside that event handler tick and thus outside the batching wrapper, so it’s processed synchronously

    To put it more clearly, get this example:

    const [counter, setCounter] = useState(0);
    
    const onClick = async () => {
      setCounter(0);
      setCounter(1);
    
      const data = await fetchSomeData();
    
      setCounter(2);
      setCounter(3);
    };
    

    In React 17 and before,

    setCounter(2);
    setCounter(3);
    

    will cause synchronous renders. Once setCounter sets the state to 2, it will cause a render. After that it will return and setState to 3, and cause another render.

    Why setCounter(2), causes a render synchronously is because:

    The call to setCounter(2) is happening after an await. The original
    synchronous call stack is empty, and the second half of the function
    is running much later in a separate event loop call stack. Because of
    that, React will execute an entire render pass synchronously as the
    last step inside the setCounter(2) call, finish the pass, and return
    from setCounter(2).

    I have picked up the explanation from a great blog blog

    Most of the above I have mentioned applies to React 17. The above behaviour is different in React 18 (and above). React 18 is even more optimized for fewer renders :D. Hence TJ sees a different output.

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