2

I noticed a behavior I cant understand how to solve, I never had it while I was writing react class based using this.setState but I get it while using useState hook.

for example:

const Main = () => {
    const [values, setValues] = useState({val1: '', val2: '', val3: ''})
    const func1 = async () => {
        await setValues({
            ...values,
            val1: '111111111'
        });
        await func2();
     }
     const func2 = async () => {
          result = (fetch some data);
          await setValues({
              ...values,
              val2: result
           });
     }
 };

now if you run func1, val1 will be changed but as soon as func2 finishes and we setValues the second time val1 will get overwritten and val2 will contain value.

what am I doing wrong and how can I fix this ?

Thanks in Advance!

Edit:

enter image description here

when using Hooks I cant see what is the acctual anme of the value entered in the React Chrome Dev tool. is there a way to fix this ? when I was having one useState containing an object I could see the titles of each object key... now its hidden -_-

user3657538
  • 205
  • 1
  • 4
  • 10
  • React Hooks works really well when you break your app into small sections,. This is avoids complex problems with closures etc, that your getting here with `values`. Eg. In your example maybe have another component called `` etc. Separation of concerns etc gets better too. Also GUI updates are more efficient too, – Keith Oct 02 '19 at 13:28
  • Possible duplicate of [useState set method not reflecting change immediately](https://stackoverflow.com/questions/54069253/usestate-set-method-not-reflecting-change-immediately) – mbojko Oct 02 '19 at 13:30

4 Answers4

3

You're spreading the state before updating the correspondent property, try to chunk your state

const Main = () => {
    const [value1, setValue1] = useState(null)
    const [value2, setValue2] = useState(null)
    const func1 = async () => {
        setValue1('foo')
        await func2();
     }
     const func2 = async () => {
          result = (fetch some data);
          await setValue2('foo')
     }
 };

Here is what is happening

  • setValues is called changing val1 (state isn't updated yet)

  • setValues is called again changing val2 and spreading the rest

By the time setValues spreads values val1 still holds it's initial value, overwriting the first setValues call. Remember, changes in state are reflected asynchronously

Dupocas
  • 20,285
  • 6
  • 38
  • 56
  • yes I thought about this solution. is there a way to fix it while keeping all of my data stacked into one object type variable ? – user3657538 Oct 02 '19 at 13:18
  • I guess you could create intermediate state object in `func1` and pass it as an argument to `func2` (instead of `func2` reading directly from `state`), but Dupocas' answer is better. – mbojko Oct 02 '19 at 13:24
  • I'm updating the answer with an alternative. But I would definitely chunk the state – Dupocas Oct 02 '19 at 13:25
  • thats not good either, I posted a simpelfied generic code, in the real code func2 is used by more then just func 1, its also called by "func3". its just more convenient to keep all my state in one object type var :\ – user3657538 Oct 02 '19 at 13:27
  • ok I think I will chunk it. can you explain to me why is this happening ? why isnt the variable being updated twice instead of overwritten ? – user3657538 Oct 02 '19 at 13:29
  • @user3657538 Because the closure on values is never updated.. If you had another render it would have been, but there is no re-render between func1 & func2. IOW: values is always the same and your spreading the same values in both cases. – Keith Oct 02 '19 at 13:32
  • Updating the answer – Dupocas Oct 02 '19 at 13:32
  • Here's an answer: https://stackoverflow.com/questions/54069253/usestate-set-method-not-reflecting-change-immediately BTW, `useState`, just like `setState` of yore, has also the callback syntax available (I forgot about that), and it would _probably_ work for you. – mbojko Oct 02 '19 at 13:33
  • `await setValue2('foo')` will not work, useState is not async. – Miller Oct 02 '19 at 13:36
  • 1
    @Miller I think that's what he means by useless.. :) – Keith Oct 02 '19 at 13:38
  • Why is it there then? Because basically it suggests that the values are updated when you log after the `setValue` – Miller Oct 02 '19 at 13:39
  • @Miller Yes, it doesn't need to be there, but that's different from saying it will not work.. Personally I agree with you, it would really want removing to save any confusion & some micro clocks.. :). – Keith Oct 02 '19 at 13:40
  • By, it will not work, i dont say the code will not run, im saying, `await` will not work, because in that case it will wait until its done and `console.log(value)` would show correct state. – Miller Oct 02 '19 at 13:41
3

React useState also takes a functional updater to update your component state similarly to how class-based component's setState works.

Note

Unlike the setState method found in class components, useState does not automatically merge update objects. You can replicate this behavior by combining the function updater form with object spread syntax:

setState(prevState => {
  // Object.assign would also work
  return {...prevState, ...updatedValues};
});

So in your code you could update as follows:

const Main = () => {
  const [values, setValues] = useState({val1: '', val2: '', val3: ''});

  const func1 = async () => {
    await setValues(prevState => ({
      ...prevState,
      val1: '111111111'
    }));
    await func2();
  }

  const func2 = async () => {
    result = (fetch some data);
    await setValues(prevState => ({
       ...prevState,
       val2: result
    }));
  }
};
Community
  • 1
  • 1
Drew Reese
  • 165,259
  • 14
  • 153
  • 181
0

I haven't tested it yet but maybe it could work as an alternative.

Sometimes, when we need to get the value in the state, we usually do the spread operator within the state but it doesn't guarantees that the value is the correct one. For those cases, we better call setState with a callback which takes the previous value from state. So, you can use something like this:

setValues(prev => ({...prev, val1:'11111'}));
Ian Vasco
  • 1,280
  • 13
  • 17
0

The behaviour of the code becomes clear once we note that values variable in func2 is clousre on values at outer scope, which is really a copy of state taken at the time useState was called.

So what your spreading in func2 is is stale copy of your state.

One way of correcting this would be to use functional update

setValues((values) => ({...values, val2: result}));

This will make sure that you are using updated value of the state.

ckedar
  • 1,859
  • 4
  • 7