A note on how React updates state





Good day, friends!



The useState () hook manages state in React functional components. In class components, state is stored in this.state, and the this.setState () method is called to update.



Usually, there is nothing difficult about working with state. However, there is one important nuance associated with updating it.



How is the state updated: immediately (synchronously) or deferred (asynchronously)? Read on to find out the answer.



1. Updating the state with useState ()



Let's say we have such a functional component:



import { useState } from 'react'

function DoubleIncreaser() {
  const [count, setCount] = useState(0)

  const doubleIncreaseHandler = () => {
    setCount(count + 1)
    setCount(count + 1)
  }

  return (
    <>
      <button onClick={doubleIncreaseHandler}>
        Double Increase
      </button>
      <div>Count: {count}</div>
    </>
  )
}

      
      





const [count, setCount] = useState (0) defines the initial state of the component. count is a variable containing the current state, and setCount is a function to update this state.



The component contains a Double Increase button. When this button is clicked, the doubleIncreaseHandler handler is called, which performs two successive updates to count: setCount (count + 1) and then setCount (count + 1) again.



What will be the state of the component after clicking the button, 1 or 2?



Open this demo and click on the button. The count value will increase by 1 after each click.



When setCount (count + 1) updates the state, the count value does not change immediately. Instead, React schedules the state to be updated and the next time it is rendered, in const [count, setCount] = useState (0), the hook assigns a new value to count.



For example: if the value of the variable count is 0, then call setCount (count + 1); setCount (count + 1) evaluates to setCount (0 + 1); setCount (0 + 1) - which results in 1 as the state value on next render.



Therefore, updating state using setValue (newValue) in [value, setValue] = useState () is done asynchronously.



However, the state update function can take a callback as an argument to calculate a new state based on the current one. In our case, we can use setCount (actualCount => actualCount + 1):



import { useState } from 'react'

function DoubleIncreaser() {
  const [count, setCount] = useState(0)

  const doubleIncreaseHandler = () => {
    setCount(actualCount => actualCount + 1)
    setCount(actualCount => actualCount + 1)
  }

  return (
    <>
      <button onClick={doubleIncreaseHandler}>
        Double Increase
      </button>
      <div>Count: {count}</div>
    </>
  )
}

      
      





When updating the state using this function, the actualCount argument will contain the actual state value.



Open this demo and click on the button. Count will increase to 2 as expected.



Of course, we can always create an intermediate variable:



import { useState } from 'react'

function DoubleIncreaser() {
  const [count, setCount] = useState(0)

  const doubleIncrease = () => {
    let actualCount = count
    actualCount = actualCount + 1
    actualCount = actualCount + 1
    setCount(actualCount)
  }

  return (
    <>
      <button onClick={this.doubleIncrease}>
        Double Increase
      </button>
      <div>Count: {count}</div>
    </>
  )
}

      
      





let actualCount = count is an intermediate variable that you can update as you like. This variable is used to update the state using setCount (actualCount).



2. The state is immutable (immutable) and read-only



If you forget that the state is updated on the next render, you can try to read the value immediately after it changes. Unfortunately, you won't get anything:



function FetchUsers() {
  const [users, setUsers] = useState([])

  useEffect(() => {
    const startFetching = async () => {
      const response = await fetch('/users')
      const fetchedUsers = await response.json()

      setUsers(fetchedUsers)
      console.log(users)        // => []
      console.log(fetchedUsers) // => ['John', 'Jane', 'Alice', 'Bob']
    }
    startFetching()
  }, [])

  return (
    <ul>
      {users.map(user => <li>{user}</li>)}
    </ul>
  )
}

      
      





The FetchUsers component sends a mount request - startFetching ().



When data is received, setUsers (fetchedUsers) updates the state. However, changes do not happen overnight.



The users variable is immutable and read-only. Only the useState () hook can assign a new value to it. You cannot do this directly:



  function FetchUsers() {
    const [users, setUsers] = useState([])

    useEffect(() => {
      const startFetching = async () => {
        const response = await fetch('/users')
        const fetchedUsers = await response.json()

        users = fetchedUsers       // ! users    
        users.push(...fetchedUsers) // ! users 
        setUsers(fetchedUsers)     // !
      }
      startFetching()
    }, [])

    return (
      <ul>
        {users.map(user => <li>{user}</li>)}
      </ul>
    )
  }

      
      





3. Updating the state in the class component



Asynchronous state updates are common for class components.



Let's consider an example:



import { Component } from 'react';

class DoubleIncreaser extends Component {
  state = {
    count: 0
  };

  render() {
    return (
      <>
        <button onClick={this.doubleIncrease}>
          Double Increase
        </button>
        <div>Count: {this.state.count}</div>
      </>
    );
  }

  doubleIncrease = () => {
    // !
    this.setState(({ count }) => ({
      count: count + 1
    }));
    this.setState(({ count }) => ({
      count: count + 1
    }));

    //  !
    // this.setState({ count: this.state.count + 1 });
    // this.setState({ count: this.state.count + 1 });
  }
}

      
      





Notice the doubleIncrease () handler: it uses a callback function to update the state.



Open this demo and click on the button. This.state value will increase to 2.



In class components, this.state is also not updated instantly. When this.setState (newState) is called, React defers updating this.state until the next render.



So this.setState (newState) updates this.state asynchronously.



4. Conclusion



The useState () hook and this.setState () (inside the class component) update the variable value and the component state asynchronously.



Remember a simple rule: calling the setState (newValue) setter of the useState () (or this.setState ()) hook does not update the state immediately, but at the next rendering of the component.



Have you noticed that React now only needs to be imported once (in index.js)? This is no longer necessary in the components.



Thank you for your attention and have a nice day.



All Articles