React Hooks: useState

☕ 5 min read
🏷️
  • #React
  • React hooks allow us to create much cleaner code than when using class components (up to 90% cleaner).

    We’ll explore the useState hook today by comparing two versions of the same counter component. One of them is written using the class syntax and the other using hooks.

    Counter (Class): https://codepen.io/alveem/pen/VwayxYx

    Counter (Hooks): https://codepen.io/alveem/pen/OJNzvqa

    Counter App Image

    Intro

    Here’s what the class based counter’s code looks like:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    
    class Counter extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          count: 0
        }
      }
      
      increase = () => {
        this.setState({ count: this.state.count + 1 })
      }
      
      decrease = () => {
        this.setState({ count: this.state.count - 1 })
      }  
      
      reset = () => this.setState({ count: 0 })
      
      render() {
        return (
          <div className="counter">
            <p className="count">{this.state.count}</p>
            <div className="controls">
              <button onClick={this.increase}>Increase</button>
              <button onClick={this.decrease}>Decrease</button>
              <button onClick={this.reset}>Reset</button>
            </div>
          </div>
        );
      }
    }
    

    And here’s what the useState version looks like:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    const Counter = () => {
      const [count, setCount] = React.useState(0);
      
      const increase = () => setCount(count + 1);
      const decrease = () => setCount(count - 1);
      const reset = () => setCount(0);
      
      return (
        <div className="counter">
          <p className="count">{count}</p>
          <div className="controls">
            <button onClick={increase}>Increase</button>
            <button onClick={decrease}>Decrease</button>
            <button onClick={reset}>Reset</button>
          </div>
        </div>
      )
    }
    

    The useState method takes in an initial state value and returns an array where the first element is a reference to the state and the second element is a function to change the state.

    Note that we’re destructuring the array to make the code cleaner. It’s the same as the following:

    1
    2
    3
    
    const countStateArray = React.useState(0);
    const count = countStateArray[0];
    const setCount = countStateArray[1];
    

    Asynchronicity

    Let’s see whether the asynchronous behavior of state change is affected by hooks.

    Class Component

    We want to increase our counter by 2 instead of 1 now. Let’s add another this.setState statement to our increase method.

    1
    2
    3
    4
    
    increase = () => {
      this.setState({ count: this.state.count + 1 })
      this.setState({ count: this.state.count + 1 })
    }
    

    But our counter still only increases by 1 because setState is asynchronous. Since setState uses Object.assign to assign the state updates it ends up doing this:

    1
    2
    3
    4
    5
    
    Object.assign(
      this.state,   // current state { count: 0 }
      { count: 1 }, // first state update
      { count: 1 }, // second state update
    )
    

    The last object with a matching key passed to Object.assign is used to update the original object’s value.

    Let’s try passing a function to setState instead:

    1
    2
    3
    4
    
    increase = () => {
      this.setState(state => ({ count: state.count + 1 }))
      this.setState(state => ({ count: state.count + 1 }))
    }
    

    And now our counter increases by 2.

    Functional Component

    Now we want to do the same using useState. We’ll call setCount a second time inside our increase method.

    1
    2
    3
    4
    
    const increase = () => {
      setCount(count + 1)
      setCount(count + 1)
    }
    

    And the counter only increases by 1. So this works the same way as when we passed an object to setState in a class component.

    Fortunately, the setCount function can also has a different form.

    1
    2
    3
    4
    
    const increase = () => {
      setCount(prevCount => prevCount + 1)
      setCount(prevCount => prevCount + 1)
    }
    

    This works now and our counter increases by 2.

    Differences

    There are some differences in the behavior of setState and the setter function returned by useState (the setCount function in our example).

    Receiving props

    The setState method passes in the props as the second argument to the callback function passed to it.

    this.setState((state, props) => console.log(props))

    State Update Behavior

    As mentioned above, setState merges objects to update state. Hooks replace the entire state.

    Let’s try to limit our counter’s decrease method so it doesn’t go below 0.

    Class Component (setState)

    Here’s what the method would look in the class component.

    1
    2
    3
    4
    5
    6
    
    decrease = () => {
        this.setState(prevState => {
          if (prevState.count <= 0) return;
          this.setState({ count: prevState.count - 1})
        })
      }
    

    Notice that on line 3 we’re not returning anything. The state still updates since React merges the old state with whatever is returned.

    Functional Component (useState)

    What happens when we do something similar in our functional component?

    1
    2
    3
    4
    
    const decrease = () => setCount(prevCount => {
        if (prevCount <= 0) return;
        return prevCount - 1;
      })
    

    When the counter is at 0 and we try to decrease it, the number disappears.

    The new state is set to undefined since whatever we return in callback function passed to setCount is set as the new state. And if we try to increase the counter we’ll get a NaN.

    Instead, you need to return the entire new state because new values aren’t automatically merged as in setState.

    1
    2
    3
    4
    
    const decrease = () => setCount(prevCount => {
        if (prevCount <= 0) return 0;
        return prevCount - 1;
      })
    

    And now it works as expected.

    If you use an object with multiple values in useState, a new object needs to be passed in that contains the old key-value pairs along with the new key-value pair(s).

    For managing complex state, the Hooks docs recommend using useReducer.

    Further Readings

    1. How Are Function Components Different From Classes?
    2. Should I Use Multiple State Variables in useState?
    3. React Hooks Docs
    4. Rules of Hooks (from the docs)
      • Don’t call Hooks inside loops, conditions, or nested functions.
      • Don’t call Hooks from regular JavaScript functions.