Update useState immediately
Asked Answered
M

6

6

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;
Murphree answered 12/3, 2021 at 20:49 Comment(6)
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 openDuvalier
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.Murphree
I think what was suggested is that you do something like if (!userQueues) return <div>Loading</div> 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.Stonwin
Where is queues defined for your return statement?Gluey
queues is declared here const queues = data.queues.map((q) => ({...Murphree
#54069753 bro, read thisBrail
M
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)
Mamelon answered 15/3, 2021 at 19:32 Comment(3)
Your code comment says "... when qeues will be updated", that is incorrect: it will be called when queues did update. It's a sutil, but impactful, difference.Weide
@Weide thanks for pointing me out. I am not native :)Mamelon
fyi... "subtle"Extrusive
L
5

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])
Lippi answered 20/3, 2021 at 7:56 Comment(0)
G
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

Glennglenna answered 19/3, 2021 at 10:16 Comment(0)
L
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
Luckin answered 16/3, 2021 at 9:5 Comment(0)
N
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);});
Nutrilite answered 19/3, 2021 at 16:12 Comment(0)
L
0

if you want to update the state and use the updated value in the same function then you can store value in a variable when setting state.

const [count, setCount] = useState(0)

const incrementCount = () => {
 let updatedCount;
 setCount((prevCount) => {
   const newCount = prevCount + 1;
   updatedCount = newCount;
   return newCount; // Return the new value
 })
 console.log(updatedCount)
};
Ludwick answered 26/10, 2023 at 19:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.