5

useState does not update the state immediately.

I'm using react-select and I need to load the component with the (multi) options selected according to the result of the request.

For this reason, I created the state defaultOptions, to store the value of the queues constant.

It turns out that when loading the component, the values ​​are displayed only the second time.

I made a console.log in the queues and the return is different from empty.

I did the same with the defaultOptions state and the return is empty.

I created a codesandbox for better viewing.

const options = [
  {
    label: "Queue 1",
    value: 1
  },
  {
    label: "Queue 2",
    value: 2
  },
  {
    label: "Queue 3",
    value: 3
  },
  {
    label: "Queue 4",
    value: 4
  },
  {
    label: "Queue 5",
    value: 5
  }
];

const CustomSelect = (props) => <Select className="custom-select" {...props} />;

const baseUrl =
  "https://my-json-server.typicode.com/wagnerfillio/api-json/posts";

const App = () => {
  const userId = 1;
  const initialValues = {
    name: ""
  };
  const [user, setUser] = useState(initialValues);
  const [defaultOptions, setDefaultOptions] = useState([]);
  const [selectedQueue, setSelectedQueue] = useState([]);

  useEffect(() => {
    (async () => {
      if (!userId) return;
      try {
        const { data } = await axios.get(`${baseUrl}/${userId}`);
        setUser((prevState) => {
          return { ...prevState, ...data };
        });

        const queues = data.queues.map((q) => ({
          value: q.id,
          label: q.name
        }));

        // Here there is a different result than emptiness
        console.log(queues);
        setDefaultOptions(queues);
      } catch (err) {
        console.log(err);
      }
    })();

    return () => {
      setUser(initialValues);
    };
  }, []);

  // Here is an empty result
  console.log(defaultOptions);

  const handleChange = async (e) => {
    const value = e.map((x) => x.value);
    console.log(value);
    setSelectedQueue(value);
  };

  return (
    <div className="App">
      Multiselect:
      <CustomSelect
        options={options}
        defaultValue={defaultOptions}
        onChange={handleChange}
        isMulti
      />
    </div>
  );
};
export default App;
Wagner Fillio
  • 395
  • 3
  • 19
  • 1
    useState triggers after a first render. if you want to start immediately with data, or you prefetch options in the state and then u render, or inside the same component you add an is loading as initial render and display after you fetched the first time. here you are running that useEffect only after thst s open – quirimmo Mar 15 '21 at 13:03
  • But here I get the data `const {data} = await api.get ('/users/${userId}'`);` and after having the data I add the result to the state `setUserQueues`. I can't do it any other way, although I really tried. – Wagner Fillio Mar 15 '21 at 13:15
  • I think what was suggested is that you do something like `if (!userQueues) return
    Loading
    ` or just nothing, so that there is no render with no info. You don't move the data to the first render. You move the first render to the data.
    – Marian Mar 15 '21 at 14:00
  • Where is `queues` defined for your return statement? – Mike Mar 15 '21 at 14:39
  • `queues` is declared here `const queues = data.queues.map((q) => ({...` – Wagner Fillio Mar 15 '21 at 15:07
  • https://stackoverflow.com/questions/54069253/usestate-set-method-not-reflecting-change-immediately bro, read this – Marcus.Aurelianus Mar 19 '21 at 16:20

5 Answers5

7

React don't update states immediately when you call setState, sometimes it can take a while. If you want to do something after setting new state you can use useEffect to determinate if state changed like this:

    const [ queues, setQueues ] = useState([])

    useEffect(()=>{
        /* it will be called when queues did update */
    },[queues] )

    const someHandler = ( newValue ) => setState(newValue)
Kishieel
  • 1,811
  • 2
  • 10
  • 19
3

Closures And Async Nature of setState

What you are experiencing is a combination of closures (how values are captured within a function during a render), and the async nature of setState.

Please see this Codesandbox for working example

Consider this TestComponent

const TestComponent = (props) => {
  const [count, setCount] = useState(0);

  const countUp = () => {
    console.log(`count before: ${count}`);
    setCount((prevState) => prevState + 1);
    console.log(`count after: ${count}`);
  };

  return (
    <>
      <button onClick={countUp}>Click Me</button>
      <div>{count}</div>
    </>
  );
};

The test component is a simplified version of what you are using to illustrate closures and the async nature of setState, but the ideas can be extrapolated to your use case.

When a component is rendered, each function is created as a closure. Consider the function countUp on the first render. Since count is initialized to 0 in useState(0), replace all count instances with 0 to see what it would look like in the closure for the initial render.

 const countUp = () => {
    console.log(`count before: ${0}`);
    setCount((0) => 0 + 1);
    console.log(`count after: ${0}`);
  };

Logging count before and after setting count, you can see that both logs will indicate 0 before setting count, and after "setting" count.

setCount is asynchronous which basically means: Calling setCount will let React know it needs to schedule a render, which it will then modify the state of count and update closures with the values of count on the next render.

Therefore, initial render will look as follows

 const countUp = () => {
    console.log(`count before: 0`);
    setCount((0) => 0 + 1);
    console.log(`count after: 0`);
  };

when countUp is called, the function will log the value of count when that functions closure was created, and will let react know it needs to rerender, so the console will look like this

count before: 0 
count after: 0 

React will rerender and therefore update the value of count and recreate the closure for countUp to look as follows (substituted the value for count).This will then update any visual components with the latest value of count too to be displayed as 1

 const countUp = () => {
    console.log(`count before: 1`);
    setCount((1) => 1 + 1);
    console.log(`count after: 1`);
  };

and will continue doing so on each click of the button to countUp.

Here is a snip from codeSandbox. Notice how the console has logged 0 from the intial render closure console log, yet the displayed value of count is shown as 1 after clicking once due to the asynchronous rendering of the UI.

enter image description here

If you wish to see the latest rendered version of the value, its best to use a useEffect to log the value, which will occur during the rendering phase of React once setState is called

useEffect(() => {
  console.log(count); //this will always show the latest state in the console, since it reacts to a change in count after the asynchronous call of setState.
},[count])
GBourke
  • 1,834
  • 1
  • 7
  • 14
2

Adding to other answers:

in Class components you can add callback after you add new state such as:

  this.setState(newStateObject, yourcallback)

but in function components, you can call 'callback' (not really callback, but sort of) after some value change such as

// it means this callback will be called when there is change on queue.
React.useEffect(yourCallback,[queue])
.
.
.

// you set it somewhere
 setUserQueues(newQueues);

and youre good to go.

no other choice (unless you want to Promise) but React.useEffect

Hadi KAR
  • 384
  • 3
  • 11
0

You need to use a parameter inside the useEffect hook and re-render only if some changes are made. Below is an example with the count variable and the hook re-render only if the count values ​​have changed.

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes
0

The problem is that await api.get() will return a promise so the constant data is not going to have it's data set when the line setUserQueues(queues); is run.

You should do:

 api.get(`/users/${userId}`).then(data=>{

  setUser((prevState) => {
    return { ...prevState, ...data };
  });

  const queues = data.queues.map((q) => ({
    value: q.id,
    label: q.name,
  }));
  setUserQueues(queues);

  console.log(queues);
  console.log(userQueues);});
Stefan
  • 9
  • 3