Managing State with React Hooks

Subscribe to my newsletter and never miss my upcoming articles

Early last year React 16.8 was released, bringing with it the addition of hooks. This introduction of hooks gave developers the ability to take many of the useful features of React (such as Lifecycle Events, Context, and State) available in class components and use them inside of function components.

In this article we will take a look at a few ways we can use hooks to manage state within function components.

If you’d like to know more about the How’s and Why’s of using function components with hooks vs. class components, check out React’s Introduction to Hooks

The application built in this tutorial is available on github


The useState Hook

The useState hook allows you to declare state variables in a function component. This data is preserved on re-renders by React exactly like a state variable in a class component is preserved. To use the hook we simply have to import it and call it, passing in an initial value.

Let’s give it a try!

Declaring a state variable with useState

First, let’s declare a state variable named points that we will use to hold a numeric point value that will be used by a point counter.

import React, { useState } from 'react'
function Points() {
    // Creates a state variable named 'points' whose initial value is 0
   const [ points, setPoints ] = useState(0)
   // ...

As we can see, useState() takes one parameter, the variable’s initial value. In this case we set the initial value to 0, however we could have assigned it an Object, Array, String, etc… Whatever you fancy!

The return value of useState() is an array containing two items. The first is the most recent value of that state variable. The second is a function used to update that state variable. We use destructuring to grab both of those items out of the return.

Updating a state variable

Now we can use the setPoints() returned by useState() to update the points state variable!

// ...
const [ points, setPoints ] = useState(0)
return (
    <>
       <button onClick={ () => setPoints( points + 1) }>Add</button>
    </>
)
// ...

In the code above, every time the user clicks the Add button, the points state variable will be incremented by one.

Putting it all together

Okay, so now we have a state variable and have the ability to update that variable! Let’s put those things together. Below is the completed Points component. Notice we can call setPoints() from within another function in the component, resetPoints() in this case.

import React, { useState } from 'react'
import '../assets/scss/Points.scss'

function Points() {
  // Creates a state variable named 'points' whose initial value is 0
  const [ points, setPoints ] = useState(0)
  // Function that will reset the point count
  const resetPoints = () => setPoints(0)

  return (
    <div id="Points">
      <p className="title">Points (useState)</p>
      <hr className="divider"/>
      <div className="pointsContainer">
          <div className="buttons">
            {/* These buttons use the setPoints function to update the state variable's value */}
            <button className="button add" onClick={() => setPoints( points + 1 )}>Add</button>
            <button className="button subtract" onClick={() => setPoints( points - 1 )}>Subtract</button>
            <button className="button reset" onClick={resetPoints}>Reset</button>
          </div>
          <div className="outputBox">
            {/* Output the points variable */}
            <p>{ points }</p>
          </div>
      </div>
    </div>
  );
}

export default Points

useStateGif-6.gif

Pretty simple stuff, right?

Using hooks, we have successfully set up and updated a state variable within a function component. Now you might be wondering, “What if I have a really complex state in my component with deeply nested data? Is there a cleaner way to set up the state?“

In the next section we’ll take a look at an alternative to useState() that addresses this exact problem!


The useReducer Hook

The useReducer hook provides the ability to manage state in a way that is very similar to how Redux manages state with reducers and dispatched actions.

If you aren’t sure what a reducer is, it is basically a function that updates a state based on an action that gets sent to it. For more detailed information, check out Redux’s documentation on reducers

To use useReducer we have to import and call it, passing in a reducer and the initial state of the component.

Setting up the state and reducer with useReducer

Let’s start by creating a simple reducer, which will define how to update the state based on certain actions, and tell the component to use that reducer.

import React, { useReducer } from 'react'

// The initial state of the component
const initialState = { points: 0 }
// The reducer we are going to use
function reducer( state, action ) {
  switch ( action.type ) {
    case 'add':
      return { points: state.points + 1 };
    default:
      throw new Error();
  }
}

function Points() {
  // Sets up a reducer to handle the state 
  const [ state, dispatch ] = useReducer( reducer, initialState );
  // ...

In the code above, we set up our initial state to be an object with a single key, points, whose value is 0. This example is simple, but useReducer really shines as the state gets more complex.

The hook takes in a reducer as its first argument and the initial state as its second argument.

With this all set up, we now have a reducer available to the component that is ready to handle actions and maintain state across re-renders!

useReducer can also take in a third argument, a function that allows you to lazily initialize the state. This can be useful if you would like to calculate the initial state outside of the reducer. Also, because this initialization function is outside of the reducer, it may be used later on to reset the state. See the docs for more info.

Dispatching actions to the reducer to update the state

useReducer() returns an array with two items. The first being the most current version of the state and the second a dispatch function. This function is used exactly the same way it would be in Redux.

// ***
return (
    <>
          <button onClick={() => dispatch({type: 'add'})}>Add</button>
    </>
);
// ***

When the button is clicked, the dispatch function is fired with a type of add, which gets caught by the reducer and results in state.points being incremented by 1.

Putting it all together

Awesome! Now let’s put all of that together. Here is a completed component using these concepts.

import React, { useReducer } from 'react'
import '../assets/scss/Points.scss'

// The initial state of the component
const initialState = { points: 0 }
// The reducer we are going to use
function reducer( state, action ) {
  switch (action.type) {
    case 'add':
      return { points: state.points + 1 };
    case 'subtract':
      return { points: state.points - 1 };
    case 'reset':
      return { points: 0 }
    default:
      throw new Error();
  }
}

function Points() {
  // Sets up a reducer to handle the state 
  const [ state, dispatch ] = useReducer( reducer, initialState );

  return (
    <div id="Points">
      <p className="title">Points (useReducer)</p>
      <hr className="divider"/>
      <div className="pointsContainer">
          <div className="buttons">
            {/* These buttons use the dispatch to update the state */}
            <button className="button add" onClick={() => dispatch({type: 'add'})}>Add</button>
            <button className="button subtract" onClick={() => dispatch({type: 'subtract'})}>Subtract</button>
            <button className="button reset" onClick={() => dispatch({type: 'reset'})}>Reset</button>
          </div>
          <div className="outputBox">
            {/* Output the points variable */}
            <p>{ state.points }</p>
          </div>
      </div>
    </div>
  );
}

export default Points

useReducerGif-1.gif

So there we have it!

A function component using a reducer to manage state. This way of managing state is preferable over useState because of some performance boosts that are mentioned in React’s documentation on the useReducer hook.


Conclusion

As you can see from the examples above, React’s implementation of hooks has not only brought over the useful feature of statefulness from class components to function components, but has also made it pretty easy to do!

We looked at two ways to keep state in a function component. First, using useState and then using useReducer. In general, useReducer is the way to go when dealing with larger data sets in the state, while useState is great for components with a simpler state.

The next step to this would be sharing state across multiple function components! In my tutorial Shared State with React Hooks and Context API I cover one of the more popular ways out there to achieve this. Give it a read if you’re interested!

Thanks for reading, I hope you enjoyed!

Edidiong Asikpo's photo

Your cover image is just as fantastic as the article. Thanks for sharing Sabin.

Bolaji Ayodeji's photo

This is amazing, thanks for sharing!

Francisco Quintero's photo

Nicely explain. Now I do wonder if there's any use in preferring class based components over hooks. What do you think, Sabin Adams?

Sabin Adams's photo

I believe this is really just a personal preference thing. Yeah, there are some potential performance boosts, one example referenced in the docs here, but the React team themselves encourage people not to go rewriting class components to functions.

Personally, I prefer Function components for a few reasons:

  1. In my opinion, they are more readable and simple.
  2. Due to the 'stateless' nature of a function component putting hooks aside for now, they encourage good modular design patterns
  3. Custom Hooks allow a nice clean way to break out reusable functionality between components
  4. See the potential performance enhancements in the link above