befreeforlife
befreeforlife

Reputation: 511

Strange behavior of React hooks: delayed data update

Strange behavior: I expect that the first and the second console.log display a different result, but they display the same result and the value is changed only on the next click. How should I edit my code so that the value will be different?

function App() {
  const [count, setCount] = React.useState(false);
  const test = () => {
    console.log(count); //false
    setCount(!count);
    console.log(count); //false
  };
  return (
    <div className="App">
      <h1 onClick={test}>Hello StackOverflow</h1>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div id="root"></div>

You can see a working example here: https://codesandbox.io/s/wz9y5lqyzk

Upvotes: 17

Views: 27989

Answers (3)

Ori Drori
Ori Drori

Reputation: 191976

The setState hook doesn't update the value in the component directly. It updates it's internal state, causes a rerender of the component, and then returns the new state value, which you assign to count.

When you console.log(count) inside test, you display the current value of count, which is the old value (before the update). If you'll move the console.log() to the render, you'll see the new value:

const { useState } = React;

const Component = () => {
  const [count, setCount] = useState(false);

  const test = () => {
    console.log('before: ', count);
    setCount(!count)
  }
  
  console.log('after: ', count);

  return (
    <div className="App">
      <button onClick={test}>Click</button>
    </div>
  );
}

ReactDOM.render(
   <Component />,
   demo
);
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

<div id="demo"></div>

Upvotes: 6

F.bernal
F.bernal

Reputation: 2684

The state update is asynchronous so if you are using Hooks you can use useEffect to be notified after an update.

Here is an example:

https://codesandbox.io/s/wxy342m6l

If you are using setState of a React component, you can use the callback

this.setState((state) => ({count: !state.count}), () => console.log('Updated', this.state.count));

Remember to use the callback to update state if you need a state value.

Upvotes: 17

T.J. Crowder
T.J. Crowder

Reputation: 1074148

State updates are asynchronous. The setCount function you get from useState can't magically reach out and change the value of your count constant. For one thing, it's a constant. For another, setCount doesn't have access to it. Instead, when you call setCount, your component function will get called again, and useState will return the updated count.

Live Example:

const {useState} = React;

function Example() {
  const [count, setCount] = useState(false);
  const test = () =>{
    setCount(!count); // Schedules asynchronous call to Example, re-rendering the component
  };
  return (
    <div className="App">
      <h1 onClick={test}>Hello CodeSandbox</h1>
      <div>Count: {String(count)}</div>
    </div>
  );
}

ReactDOM.render(<Example />, document.getElementById("root"));
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>

If you need to perform some side-effect when count changes, use useEffect:

useEffect(() => {
  console.log(`count changed to ${count}`);
}, [count]);

Notice that we tell useEffect to only call our callback when count changes, so that if we have other state, our callback doesn't run unnecessarily.

Live Example:

const {useState, useEffect} = React;

function Example() {
  const [count, setCount] = useState(false);
  const test = () =>{
    setCount(!count); // Schedules asynchronous call to Example, re-rendering the component
  };
  useEffect(() => {
    console.log(`count changed to ${count}`);
  }, [count]);
  return (
    <div className="App">
      <h1 onClick={test}>Hello CodeSandbox</h1>
      <div>Count: {String(count)}</div>
    </div>
  );
}

ReactDOM.render(<Example />, document.getElementById("root"));
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>

Upvotes: 9

Related Questions