admin管理员组

文章数量:1126448

I am trying to learn hooks and the useState method has made me confused. I am assigning an initial value to a state in the form of an array. The set method in useState is not working for me, both with and without the spread syntax.

I have made an API on another PC that I am calling and fetching the data which I want to set into the state.

Here is my code:

<div id="root"></div>

<script type="text/babel" defer>
// import React, { useState, useEffect } from "react";
// import ReactDOM from "react-dom";
const { useState, useEffect } = React; // web-browser variant

const StateSelector = () => {
  const initialValue = [
    {
      category: "",
      photo: "",
      description: "",
      id: 0,
      name: "",
      rating: 0
    }
  ];

  const [movies, setMovies] = useState(initialValue);

  useEffect(() => {
    (async function() {
      try {
        // const response = await fetch("http://192.168.1.164:5000/movies/display");
        // const json = await response.json();
        // const result = json.data.result;
        const result = [
          {
            category: "cat1",
            description: "desc1",
            id: "1546514491119",
            name: "randomname2",
            photo: null,
            rating: "3"
          },
          {
            category: "cat2",
            description: "desc1",
            id: "1546837819818",
            name: "randomname1",
            rating: "5"
          }
        ];
        console.log("result =", result);
        setMovies(result);
        console.log("movies =", movies);
      } catch (e) {
        console.error(e);
      }
    })();
  }, []);

  return <p>hello</p>;
};

const rootElement = document.getElementById("root");
ReactDOM.render(<StateSelector />, rootElement);
</script>

<script src="/@babel/standalone@7/babel.min.js"></script>
<script src="@17/umd/react.production.min.js"></script>
<script src="@17/umd/react-dom.production.min.js"></script>

I am trying to learn hooks and the useState method has made me confused. I am assigning an initial value to a state in the form of an array. The set method in useState is not working for me, both with and without the spread syntax.

I have made an API on another PC that I am calling and fetching the data which I want to set into the state.

Here is my code:

<div id="root"></div>

<script type="text/babel" defer>
// import React, { useState, useEffect } from "react";
// import ReactDOM from "react-dom";
const { useState, useEffect } = React; // web-browser variant

const StateSelector = () => {
  const initialValue = [
    {
      category: "",
      photo: "",
      description: "",
      id: 0,
      name: "",
      rating: 0
    }
  ];

  const [movies, setMovies] = useState(initialValue);

  useEffect(() => {
    (async function() {
      try {
        // const response = await fetch("http://192.168.1.164:5000/movies/display");
        // const json = await response.json();
        // const result = json.data.result;
        const result = [
          {
            category: "cat1",
            description: "desc1",
            id: "1546514491119",
            name: "randomname2",
            photo: null,
            rating: "3"
          },
          {
            category: "cat2",
            description: "desc1",
            id: "1546837819818",
            name: "randomname1",
            rating: "5"
          }
        ];
        console.log("result =", result);
        setMovies(result);
        console.log("movies =", movies);
      } catch (e) {
        console.error(e);
      }
    })();
  }, []);

  return <p>hello</p>;
};

const rootElement = document.getElementById("root");
ReactDOM.render(<StateSelector />, rootElement);
</script>

<script src="https://unpkg.com/@babel/standalone@7/babel.min.js"></script>
<script src="https://unpkg.com/react@17/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>

Neither setMovies(result) nor setMovies(...result) works.

I expect the result variable to be pushed into the movies array.

Share Improve this question edited Apr 7, 2022 at 12:49 Peter Mortensen 31.6k22 gold badges109 silver badges133 bronze badges asked Jan 7, 2019 at 6:05 PranjalPranjal 9,0033 gold badges9 silver badges13 bronze badges 1
  • 1 Are you able to see the changes moving console.log("movies =", movies); outsite the useEffect hook? – Domenico Ruggiano Commented Apr 3, 2021 at 8:33
Add a comment  | 

21 Answers 21

Reset to default 962

Much like .setState() in class components created by extending React.Component or React.PureComponent, the state update using the updater provided by useState hook is also asynchronous, and will not be reflected immediately.

Also, the main issue here is not just the asynchronous nature but the fact that state values are used by functions based on their current closures, and state updates will reflect in the next re-render by which the existing closures are not affected, but new ones are created. Now in the current state, the values within hooks are obtained by existing closures, and when a re-render happens, the closures are updated based on whether the function is recreated again or not.

Even if you add a setTimeout the function, though the timeout will run after some time by which the re-render would have happened, the setTimeout will still use the value from its previous closure and not the updated one.

setMovies(result);
console.log(movies) // movies here will not be updated

If you want to perform an action on state update, you need to use the useEffect hook, much like using componentDidUpdate in class components since the setter returned by useState doesn't have a callback pattern

useEffect(() => {
    // action on update of movies
}, [movies]);

As far as the syntax to update state is concerned, setMovies(result) will replace the previous movies value in the state with those available from the async request.

However, if you want to merge the response with the previously existing values, you must use the callback syntax of state updation along with the correct use of spread syntax like

setMovies(prevMovies => ([...prevMovies, ...result]));

Additional details to the previous answer:

While React's setState is asynchronous (both classes and hooks), and it's tempting to use that fact to explain the observed behavior, it is not the reason why it happens.

TLDR: The reason is a closure scope around an immutable const value.


Solutions:

  • read the value in render function (not inside nested functions):

      useEffect(() => { setMovies(result) }, [])
      console.log(movies)
    
  • add the variable into dependencies (and use the react-hooks/exhaustive-deps eslint rule):

      useEffect(() => { setMovies(result) }, [])
      useEffect(() => { console.log(movies) }, [movies])
    
  • use a temporary variable:

      useEffect(() => {
        const newMovies = result
        console.log(newMovies)
        setMovies(newMovies)
      }, [])
    
  • use a mutable reference (if we don't need a state and only want to remember the value - updating a ref doesn't trigger re-render):

      const moviesRef = useRef(initialValue)
      useEffect(() => {
        moviesRef.current = result
        console.log(moviesRef.current)
      }, [])
    

Explanation why it happens:

If async was the only reason, it would be possible to await setState().

However, both props and state are assumed to be unchanging during 1 render.

Treat this.state as if it were immutable.

With hooks, this assumption is enhanced by using constant values with the const keyword:

const [state, setState] = useState('initial')

The value might be different between 2 renders, but remains a constant inside the render itself and inside any closures (functions that live longer even after render is finished, e.g. useEffect, event handlers, inside any Promise or setTimeout).

Consider following fake, but synchronous, React-like implementation:

// sync implementation:

let internalState
let renderAgain

const setState = (updateFn) => {
  internalState = updateFn(internalState)
  renderAgain()
}

const useState = (defaultState) => {
  if (!internalState) {
    internalState = defaultState
  }
  return [internalState, setState]
}

const render = (component, node) => {
  const {html, handleClick} = component()
  node.innerHTML = html
  renderAgain = () => render(component, node)
  return handleClick
}

// test:

const MyComponent = () => {
  const [x, setX] = useState(1)
  console.log('in render:', x) // ✅
  
  const handleClick = () => {
    setX(current => current + 1)
    console.log('in handler/effect/Promise/setTimeout:', x) // ❌ NOT updated
  }
  
  return {
    html: `<button>${x}</button>`,
    handleClick
  }
}

const triggerClick = render(MyComponent, document.getElementById('root'))
triggerClick()
triggerClick()
triggerClick()
<div id="root"></div>

I know that there are already very good answers. But I want to give another idea how to solve the same issue, and access the latest 'movie' state, using my module react-useStateRef it has 11,000+ weekly downloads.

As you understand by using React state you can render the page every time the state change. But by using React ref, you can always get the latest values.

So the module react-useStateRef let you use state's and ref's together. It's backward compatible with React.useState, so you can just replace the import statement

const { useEffect } = React
import { useState } from 'react-usestateref'

  const [movies, setMovies] = useState(initialValue);

  useEffect(() => {
    (async function() {
      try {

        const result = [
          {
            id: "1546514491119",
          },
        ];
        console.log("result =", result);
        setMovies(result);
        console.log("movies =", movies.current); // will give you the latest results
      } catch (e) {
        console.error(e);
      }
    })();
  }, []);

More information:

  • react-usestsateref

Most of the answers here are about how to update a state based on its previous value, but I don't understand how that relates to the question

The useState set method is not reflecting a change immediately


React 18

useState is asynchronous:

When an event that triggers a certain code, occurs, the code starts running, and when it finshes, react will check if there was a state update and if it is the case, only then the value of the useState hook is updated and this leads to a new render in which the new value is availabe.

const [example,setExemple] = useState("")
//...
<button
  onClick={() => {
    const newValue = "new";
    setExample(newValue);
    console.log(example); // output "" and this is normal, because the component didn't rerenderd yet so the new value is not availabe yet
  }}
>
  Update state
</button>

Supposing we have a scenario where we have a state which depends on another state, for example we want to make an API call based on the new value of example every time it is updated and then store the data from response in another state anotherExample.
to achieve so we have two ways:

1. use the value of newValue:

<button
  onClick={async () => {
    const newValue = "new";
    const response = await axios.get(`http://127.0.0.1:5000/${newValue}`);
    setExample(newValue);
    setAnotherExample(response.data);
  }}
>
  test
</button>

since you know that example will receive this value, you can create your logic based on it directly.

2. trigger a useEffect to run each time example is updated by including example in its dependency array:

<button
  onClick={() => {
    const newValue = "new";
    setExample(newValue);
  }}
>
  test
</button>
useEffect(() => {
 async function test(){
  const response = await axios.get(`http://127.0.0.1:5000/${example}`);
  setAnotherExample(response.data);
 } 
 test();
}, [example])

so when example is updated with the event function the component rerenders, we are now in a new different render that once finished, useEffect will run because the value of example is different from what is was during the last render, and since it is a new different render, the new value of example useState hook is available here.

Note: the useEffect hook will run anyway during the first mount.

Which approach better?