2

I have the following problem.

function App() {
    const [state, setState] = useState({ a: 0 });

    const handleClick = () => {
        Promise.resolve().then(() => {
            const _state = { a: 1 };

            console.log("1");
            setState(_state);

            console.log("2");
            _state.a = 2;
            setState(_state);

            console.log("3");
            _state.a = 3;
            setState(_state);
        });
    };

    return (
        <div>
            <button onClick={handleClick}>Test</button>
            <br />
            {console.log("render", state)}
            {state.a}
        </div>
    );
}

After I click the button, following output is shown in the console:

1
render {a: 1}
2
render {a: 2}
3

But it renders {a: 1} instead of {a: 2}

So at the end state is changed and its value is {a: 3}, but react doesn't render new value.In react devtools I can clearly see state: {a: 3}. If I remove Promise.resolve the code will work as expected.

Additional question is, why does it invoke render in the middle of the code (after console.log(1)?

Here is the full example: https://codesandbox.io/s/exciting-platform-0j45u

  • I updated my answer so hopefully it makes it clear why `render` doesn't show the latest value and why `renders` are triggered on every `setState` call. – goto Mar 09 '20 at 13:26

2 Answers2

2

In promise callback, you set the state only once on the first setState(_state);, on every other try, the component doesn't render because as React rendering works, it makes shallow comparison with the previous state, which is always true because it holds the same object reference (it's javascript):

const state = { a: 1};
setState(state); // prevState = state;

state.a = 2;
prevState === state // always true so not render is triggered

Its a common beginner mistake, React states to treat state as immutable..

Dennis Vash
  • 50,196
  • 9
  • 100
  • 118
  • Yes I did know that. But the problem is that state is changed, its value is now `{a: 3}` but it renders old value? – Haris Palic Mar 09 '20 at 13:11
  • Changing the value doesn't trigger render, diff state is one reason to trigger render. – Dennis Vash Mar 09 '20 at 13:12
  • I understand now, thank you. And regarding the second part of my question, why does it invoke render in the middle of the code? – Haris Palic Mar 09 '20 at 13:16
  • Because `setState` is async (check the docs), so after a few seconds of calling the first `setState`, when the conditions are met, the component will render. – Dennis Vash Mar 09 '20 at 13:17
2

The reason why React is not rendering the latest state is because you're directly manipulating state by doing the following:

const _state = { a: 1 };

setState(_state);

// you're using the same object
_state.a = 2;
setState(_state);

// you're using the same object here too
_state.a = 3;
setState(_state);

You should not be mutating state directly and you should treat state as it were immutable.

To fix this, you need to make a copy of your state, update whatever properties you want, and then use that new object to update your state.

const handleClick = () => {
  Promise.resolve().then(() => {
    const _state = { a: 1 };

    console.log("1");
    setState(_state);

    console.log("2");
    const _state2 = { ..._state, a: 2 };
    setState(_state2);

    console.log("3");
    const _state3 = { ..._state, a: 3 };
    setState(_state3);
    console.log("3");
  });
};

Here's a working example:


Also, React will batch state updates so sometimes if you're not seeing "correct behavior" when you call setState multiple times in a row for the same piece of state it's because React is trying to prevent unnecessary re-renders by batching those updates. In your case, however, they will not be batched since they are triggered in a Promise.

See here for more information on how batching works in regards to state updates:

goto
  • 4,336
  • 15
  • 20