Update useState immediately

Viewed 3719

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;
5 Answers

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)

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

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])

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

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);});
Related