fbpx Skip to content

Aquent | DEV6

State Management with React Hooks – Part 1

Written by: Ali Hamoody

In this 2-part series, I will share with you some simple state management patterns in React functions using core features, without using classes or external JavaScript libraries.

TL; DR

Part 1

  • Use the useState() hook to manage the local state of a single variable.
  • Use the useReducer() hook to manage the local state of multiple related variables.

Part 2

  • For bi-directional state sharing between parent-child components, use props that contain the state variable as well as the setter function.
  • To share state between multiple components that are not directly related, or globally across your application, combine useState() or useReducer() with React Context API and useContext() hook.

Introduction

I will skip the usual introduction to React Hooks and just say that React Hooks are awesome. Don’t just take my word for it, try it and decide for yourself.

If you haven’t heard of React Hooks and want to know more about them, I suggest you start with the blogs written by my colleagues Leonard Lacson:An Introduction to React Hooks and Peter Wang’s blog: Demystify React Hooks, in addition to the React Hooks official documentation.

In this blog, I will dive straight into state management, and will try to keep it short. We will not use React classes and you don’t have to know state management techniques used in classes (like setState), nor external libraries like Redux. However, basic knowledge of React and function components is required.

Local State Management

In React, function components receive props, implement their own logic, and return JSX.

Each render causes the function to run and return a new JSX, that is used to update the DOM and the UI if the new JSX is different than the one from the previous render

When the function executes, the values of the constants and variables are not available from the previous run (or render), and therefore the function starts from scratch every time. The only data it starts with are the props values passed into it.

If your function needs to know the values of the variables or constants from the previous render, then you need to store that “state”.

useState

The hook useState() is used to manage the state of a single primitive (Boolean, string or a number), or a single object.

For example, useState() can be used in forms to remember the values of input field values as the user types and causes re-renders. Check out this code:

http://codesandbox.io/s/react-state-management-1887i

Let’s break it down:

1. Import the useState hook.

import { useState } from "react";

2. Use the useState hook to create the state variable “name” and a function to set its value “setName”. The initial value is set to an empty string “”.

const [name, setName] = useState("");

3. To display the value of the “name” state:

<h1>Hello {name}</h1>

4. To change the value of the “name” state variable

setName(event.target.value)

That’s pretty much it.

Spend some time to experiment with this in CodeSandbox, then come back to continue…

Keep in mind that useState can be used for objects as well, in addition to primitives like strings, numbers and Boolean data types.

It is important also to know that the setter function replaces the existing value with the new one, not merge into its previous state. This is critical to remember when setting values for objects. However, you can easily mimic the merge operation if you need it by using the ES6 spread operator.

useReducer

Now let’s move on to a slightly more complex scenario.

If your application needs to control two or more different but related state variables, then you will probably want to swap the useState hook with useReducer.

In this case, we need to provide a reducer function to the useReducer hook. The reducer function receives a state and an action object and returns a new state object.

See the React documentation for more details: https://reactjs.org/docs/hooks-reference.html#usereducer

Time for an example. Let’s pretend we need to keep track of the login state in your application with a flag, a user name and a timestamp.

So, our initial state object looks like this:

const initState = {
  isLoggedIn: false,
  userName: "",
  loginTime: null
};

Let’s define a reducer function that receives an action and state. It implements the logic that produces the next state. We need to support two action types, LOGIN and LOGOUT.

const loginReducer = (state, action) => {
  switch (action.type) {
    case "LOGIN":
      if (state.isLoggedIn) throw new Error("Already logged-in!");
      return {
        isLoggedIn: true,
        userName: action.userName,
        loginTime: new Date().toTimeString()
      };

    case "LOGOUT":
      if (!state.isLoggedIn) throw new Error("Not logged-in!");
      return initState;

    default:
      throw new Error("Unknown Action");
  }
};

The reducer function returns an object that contains the new login state. It uses the “action” input parameter data to apply changes to the state.

Define the hook useReducer, that uses the loginReducer function as follows:

const [loginState, loginDispatch] = useReducer(loginReducer, initState);

The useReducer hook takes in a reducer function and an initial state. It returns an array containing the state object and a dispatch function.

Compare this to the useState hook that takes in an initial state, and returns an array containing the state object and a setter function.

  const [state, setState] = useState(initState);

Do you see how similar they are? The difference is the existence of the reducer function.

You can think of the dispatch function as the setter function on steroids, with a richer logic and functionality.

Let’s put the loginState and loginDispatch to use.

I created a login button and a logout button to simulate login and logout operations. Note that in this example, we are keeping it simple and won’t be implementing actual authentication logic.

Login Button

<button
  type="button"
  onClick={() => loginDispatch({ type: "LOGIN", userName: name })}
  >              
  Login
</button>

Logout Button

<button
  type="button"
  onClick={() => loginDispatch({ type: "LOGOUT" })}
  >
    Logout
</button>

To see the state values, let’s add a header area to show the login information on the top left corner of the application.

<div className="LoginInfo">
        {loginState.isLoggedIn ? (
          <>
            <div>Welcome {loginState.userName}</div>
            <div>Logged in at {loginState.loginTime}</div>
          </>
        ) : (
          <>
            <div>Guest</div>
            <div>Please login</div>
          </>
        )}
      </div>

This code snippet also shows one way of performing conditional rendering. That is using a ternary operator to show the login information when the user is logged in or show a guest message otherwise.

Complete Source Code: http://codesandbox.io/s/react-state-management-3dd4g

Try it in CodeSandbox, and feel free to make changes and experiment.

Pretty neat, isn’t it? And all done in pure React!

In real production-grade applications, it won’t be as simple, but the pattern and concepts still apply just the same. In fact, the example above was inspired by a real-world application, after removing the API calls and simplifying quite a bit.

I hope you enjoyed this blog and picked up a trick or two along the way.

Thanks for staying with me so far!

In Part 2 of this series, I will talk about how to pass the state values across multiple components, directly between parent-child components, and globally throughout the application.

References

https://reactjs.org/docs/hooks-reference.html

An Introduction to React Hooks

Demystify React Hooks