Tuesday, 2 November, 2021 UTC


Summary

React.js has undergone massive developments in recent times, resulting in various state management libraries. With React projects making use of an enormous code base, there is a need to centralize and maintain code and handle data flow across the application. State Management manages code and data maintenance, improving code quality and data sharing between application components. We will discuss the best choices for state management in this article.
Goals
In this article, we will discuss the terms state and state management. We will also talk about different state management methods; using Redux, Recoil, and Context API.
What is State and State Management
State in JavaScript applications simply refers to all data generated through user actions. It is a plain JavaScript object that holds information used by React to render components in which its content is dynamic depending on the resultant action of users. React applications, whether functional or class components can have stated. In react applications, data can be passed between components through a method called "Prop-drilling". This is when data is passed continuously from parents to descendants. But as the application gets larger, the need for a global state to hold and share states arises.
In large applications such as e-commerce sites, when users interact with a certain component changes are effected into components in the application. For instance:
  • Adding a shopping item to a cart requires the resulting data to be passed from the shopping component to the cart component.
  • Add purchased product to user purchase history
  • Checkout of items in the cart
  • In complex apps, these changes can be difficult to track, therefore the need for a structure to handle data flow and manage communication between our app’s components. State management is a definite structure for managing communication and sharing of states in our application.
  • State management libraries provide us with tools to create and manage updates of these structures. Over the years, different state management libraries have been developed. Knowing what state management libraries to use is crucial to the development of an application. In this tutorial, we will be looking at some state management libraries and how they can be used.
Hooks
There are many methods of managing state in React applications. You can opt for class-based state management, React Hooks or third-party libraries like Redux or Recoil. In this section, we'll talk about how we can manage state using Hooks which is encouraged by the React Docs.
According to the React Docs, Hooks are basically functions that let you access state and other React features without using a class component. So no single object holds all the state of the component. This allows you to split state into chunks that can be independently updated.
In this section, we'll be looking at a few of the most important Hooks used to manage state. We'll talk about setting state using the useState and useReducer Hooks.
The useState Hook
The useState Hook is useful for setting and updating state, similar to this.state in a a class component. With useState you can set the value without referencing the current state.
const [state, setState] = useState(initialState);
useState is a function that takes in the initial state (initialState in the code above) as an argument. It returns an array of two items which we obtain and assign their values to any variable name we choose using destructuring.
The first item ( state ) from the returned array is a variable containing the state which you can use in your component.
The second item from the array is a function ( setState ) that will update the state.
Let's have a look at this Hook in action with this example Movies component.
import React, { useState } from "react";

// array of movie objects
const movies = [
  {
    title: "Black Widow",
    price: "12.00",
    rating: "4.0"
  },
  {
    title: "Justice Leage - Snyder Cut",
    price: "10.00",
    rating: "4.9"
  }
];

export default function Movies() {
  // create state variables with initial values
  const [bookedMovies, setBookedMovies] = useState([]);
  const [totalPrice, setTotalPrice] = useState(0);

  // functions to set the state
  const add = () => {
    // set state for bookedMovies
    setBookedMovies([movies[0]]);
    // set state for totalPrice
    setTotalPrice(movies[0].price);
  };

  // reset state values
  const remove = () => {
    // reset values
    setBookedMovies([]);
    setTotalPrice(0);
  };

  return (
    <div>
      <header>
        <p> No. of movies: {bookedMovies.length} </p>
        <p> Total price: ${totalPrice} </p>
      </header>

      <ul className="movies">
        {movies.map((movie) => {
          return (
            <li className="movie" key={movie.name}>
              <header>
                <h3> {movie.title} </h3>
                <p> ${movie.price} </p>
              </header>

              <button onClick={add}> Add </button>
              <button onClick={remove}> Remove </button>
            </li>
          );
        })}
      </ul>
    </div>
  );
}
// BeforeUnloadEvent(()=>{
//   alert('before now')
// })
Here, we imported the useState Hook from React. Then we initialized two independent states of data: bookedMovies and totalPrice.
This is a huge advantage of useState hooks over class-based state management, which provides only a single state object. With Hooks, we can have multiple and independent state objects by just calling the useState function, provide an initial state or value and assign the items of the array to new variables.
Let's see how we can set the value of our state. Here, we'll create a function that will allow users to update our state with some predefined data. First we'll create a function that will add a new movie to our bookedMovies array and set the price.
...

export default function Movies() {
  // create state variables with initial values
  const [bookedMovies, setBookedMovies] = useState([]);
  const [totalPrice, setTotalPrice] = useState(0);

  // functions to set the state
  const add = () => {
    // set state for bookedMovies
    setBookedMovies([movies[0]]);
    // set state for totalPrice
    setTotalPrice(movies[0].price);
  };

  // reset state values
  const remove = () => {
    // reset values
    setBookedMovies([]);
    setTotalPrice(0);
  };

  return (
    <div>
      <header>
        <p> No. of movies: {bookedMovies.length} </p>
        <p> Total price: ${totalPrice} </p>
      </header>

      <ul className="movies">
        {movies.map((movie) => {
          return (
            <li className="movie" key={movie.name}>
              <header>
                <h3> {movie.title} </h3>
                <p> ${movie.price} </p>
              </header>

              <button onClick={add}> Add </button>
              <button onClick={remove}> Remove </button>
            </li>
          );
        })}
      </ul>
    </div>
  );
}
Edit on CodeSandbox
In the code above, we created two functions, add() and remove. The add() function uses the function setBookedMovies to set the value of bookedMovies and `setTotalPrice` to set the value of totalPrice.
We're calling this add() function using the onClick event listener on the Add button.
Once the button is clicked, bookedMovies is assigned the value of the first movie in the movies list ( "Black Widow" ) and totalPrice now contains the price of the first movie ( 12.00 ).
On the other hand, the remove() function sets bookedMovies and totalPrice to an empty array and 0 respectively.
Great! We've seen how we can set and update state data with useState. But we're using static, hard-coded values. In real-world applications, you may need to use the previous state to set the new state instead of overwriting it. Let's see how we can update the state using the current state with the useReducer hook.
The useReducer Hook
So far we've been updating our state and overwriting the previous state with static values. However, in a real application, you'll want to be able to update the booked movies with the previous items in place.
With useReducer we'll be able to update the state based on the previous state. This Hook is designed to update the state based on the current state like the Array Reduce method.
First, we'll create a bookedMoviesReducer function which takes in two arguments, state, and action.state is the current state and action is an object containing two properties, the movie object and the type of action to be performed on the state - "add" or "remove".
...

function bookedMoviesReducer(state, action) {

    // get the movie object and the type of action by destructuring
    const { movie, type } = action

    // if "add"
    // return an array of the previous state and the movie object
    if (type === "add") return [...state, movie]

    // if "remove"
    // remove the movie object in the previous state
    // that matches the title of the current movie object
    if (type === "remove") {
        const movieIndex = state.findIndex( x => x.title === movie.title)

        // if no match, return the previous state
        if(movieIndex < 0 ) return state 

        // avoid mutating the original state, create a copy
        const stateUpdate = [...state]

        // then splice it out from the array
        stateUpdate.splice(movieIndex, 1)
        return stateUpdate
    }
    return state
}

...

export default function Movie(){
    ...
}
Then, we have a totalPriceReducer function which takes the same arguments as the bookedMoviesReducer but just adds and subtracts the value of the previous state to and from the new one respectively.
... 

function totalPriceReducer(state, action) {
    const{ price, type } = action
    if (type === "add") return state + price

    // return the value when the type of action was not "add"
    // subract the new movie price from the previous state
    return (state - price) < 0 ? 0 : ( state - price )
}

...

export default function Movie(){
    ...
}
Note the ternary operator in the return statement above. This resets the calculation to 0 if state - price is ever lesser than 0 (negative).
Now that we have our refactored functions, let's use them in useReducer
import React, { useReducer } from 'react';

...

export default function Movies() {
  // replace useState with useReducer and pass two arguments
  // the first argument is the reducer function
  // the second function is the initialState
  const [bookedMovies, setBookedMovies] = useReducer(bookedMoviesReducer, []);
  const [totalPrice, setTotalPrice] = useReducer(totalPriceReducer, 0);

  // function to add items and price
  const add = (movie) => {
    // pass an object containing the movie and type of action, "add"
    setBookedMovies({ movie, type: "add" });
    setTotalPrice({ price: movie.price, type: "add" });
  };

  // function to remove items and price
  const remove = (movie) => {
    setBookedMovies({ movie, type: "remove" });
    setTotalPrice({ price: movie.price, type: "remove" });
  };

  return (
    ...
  )
}
...
Now that we've edited our add() and remove() function to take a movie argument, we can pass in the movie as the argument in our onClick listener in our template.
...

{movies.map((movie) => {
  return (
    <li className="movie" key={movie.name}>
      <header>
        <h3> {movie.title} </h3>
        <p> ${movie.price} </p>
      </header>

      <button onClick={() => add(movie)}> Add </button>
      <button onClick={() => remove(movie)}> Remove </button>
    </li>
  );
})}
...
Edit on CodeSandbox
Now if we click on the Add button, it runs the add() function which takes movie as an argument which is in turn passed into the setBookedMovies and setTotalPrice functions. Each of these functions are returned from their respective useReducer functions.
const [ bookedMovies, setBookedMovies ] = useReducer(bookedMoviesReducer, []);
const [ totalPrice, setTotalPrice ] = useReducer(totalPriceReducer, 0);
Each function takes an object as an argument.
For setBookedMovies, it takes this object - {movie, type: "add"} as an argument, while, setTotalPrice takes this object - {price: movie.price, type: "add"} as an argument.
These objects are then passed into their respective reducer functions to update the state. setBookedMovies for example passes its object into bookedMoviesReducer.
This reducer function adds this new data to the previous state without overwriting it.
You can check out the CodeSandbox above to see this in action.
Next, we'll see how we can manage state across multiple components using Hooks, Props, and the Context API.
Context API
React Context API was an experimental feature of React that became available to use for production in version 16.3.0 of React.
According to the docs, Context provides a way to pass data through and between components in your app's component tree without having to manually pass props down through multiple component levels.
This manual passing of props is often referred to as prop drilling.
With Context, you can set up a "global" state for a tree of React components. This state can now be accessible from any component without having to pass it through intermediate components.
To illustrate how we can share state between components using Context API, we'll break the app in the example above into two sub-components:
  • BookedMovies - This would contain the total number of booked movies and the total price
  • Movies - This renders a list of all available movies, their details, and then add and remove buttons for adding and removing the movie from the booked list.
Currently, all our pieces of state, the movies including bookedMovies and totalPrice which we set using Hooks are all in one component - App.js.
For our state to be accessible to our child components, we can pass it down the data using props.
This implies that we have to lift the state up to the topmost component of our component tree and pass it down to the component that needs to access the state. Now, this can be two or more levels down the component tree which takes us back to prop drilling.
Instead of doing all that, we can initialize Context and set up a "global" state that can be accessed from any component on the tree.
Initialize Context
To initialize context, we can create a new file MoviesContext.js specifically for our Context, import createContext, and create our Context.
import React, {useReducer, createContext} from 'react';export const MovieContext =  createContext();
Create Context Provider
Now that we've initialized Context, we can create a Context Provider. According to the docs, every Context object comes with a Provider component that allows components wrapped within it to subscribe to context changes.
import React, { useState, useReducer, createContext } from "react";

const moviesList = [
 ...
];

function bookedMoviesReducer(state, action) {
  ...
};

export const MovieContext = createContext();

export const MovieProvider = (props) => {
  const [movies, setMovies] = useState(moviesList);
  const [bookedMovies, setBookedMovies] = useReducer(bookedMoviesReducer, []);

  return <MovieContext.Provider>{props.children}</MovieContext.Provider>;
};
Here, we've moved all our state and reducer functions into our Context file MoviesContext.js. Then in our MovieProvider function, we initialize the state using useState and useReducer Hooks.
Finally, we're returning the Provider component <MovieContext.Provider>. props.children will allow us to render the components that will be nested within the provider.
Here's our refactored App.js file with imported components and Context.
// ./App.js

import React from "react";
import Movies from "./components/Movies";
import BookedMovies from "./components/BookedMovies";
import { MovieProvider } from "./moviesContext";

export default function App() {
  return (
    <MovieProvider>
      <main>
        <Movies />
        <BookedMovies />
      </main>
    </MovieProvider>
  );
}
Great. We've managed to create a provider and wrap our components with the provider in our app. We currently don't have access to any of our state from the Context. Let's see how we can consume or use the state.
Consuming our state
The Provider component accepts a value prop that can be accessed by components that are descendants of the Provider. We have to pass all our state into our app in our Context file MoviesContext.js
// ./MoviesContext.js
...
  return (
    <MovieContext.Provider
      value={{
        movies,
        setMovies,
        bookedMovies,
        setBookedMovies,
      }}
    >
      {props.children}
    </MovieContext.Provider>
  );
...
To subscribe to these pieces of state in our components, we have to import MovieContext and consume it with the useContext Hook. Here's how we can do that in our Movies component.
// ./components/Movies.js

// import the useContext Hook
import React, { useContext } from "react";
// import the Context
import { MovieContext } from "../moviesContext";

export default function Movies() {
  const { movies, setBookedMovies } = useContext(MovieContext);
  // function to add items and price
  const add = (movie) => {
    // pass an object containing the movie and type of action, "add"
    setBookedMovies({ movie, type: "add" });
  };

  // function to remove items and price
  const remove = (movie) => {
    setBookedMovies({ movie, type: "remove" });
  };

  return (
    <ul className="movies">
      {movies.map((movie) => {
        return (
          <li className="movie" key={movie.name}>
            <header>
              <h3> {movie.title} </h3>
              <p> ${movie.price} </p>
            </header>

            <button onClick={() => add(movie)}> Add </button>
            <button onClick={() => remove(movie)}> Remove </button>
          </li>
        );
      })}
    </ul>
  );
}
Now we can access all our state from this component. We can also do the same thing for our BookedMovies component.
// ./components/BookedMovies.js

import React, { useContext } from "react";
import { MovieContext } from "../moviesContext";

function BookedMovies() {
  const {bookedMovies} = useContext(MovieContext);

  const getTotalPrice = (bookedMovies) => {
    const totalPrice = bookedMovies.reduce((totalCost, item) => totalCost + item.price, 0);
    return totalPrice
  }

  return (
    <header>
      <p> No. of movies: {bookedMovies.length} </p>
      <p> Total price: ${getTotalPrice(bookedMovies)} </p>
    </header>
  );
}

export default BookedMovies;
Edit on CodeSandbox
There we go. We've managed to subscribe to bookedMovies state from our Context in this component. Also, we've refactored the total price functionality and created a new function getTotalPrice that will get the price from each movie on the bookedMovie array and total it.
That's how we can manage state using the Context API and Hooks.
In the next sections, we'll see how we can manage state using other popular libraries like Redux and Recoil.
Redux
What is Redux?
Redux as the doc implies is a container for Javascript apps. It allows us to manage our app state, track and manage changes as the app progresses. Redux allows React components to read data from a Redux store and dispatch actions to the store to update this data. Here is a diagrammatic representation of how Redux works:

The UI is updated based on the changes made to the state stored in the store thereby making it easier to continuously update as changes are made.
Redux provides a central store where our state resides. Each component in the application can then access the stored data without having to send it from one component to another.
When should we use Redux?
You can use Redux in your application when:
  • There are large amounts of states in the app that undergo frequent updates. It solves the problem of prop drilling between components and makes it easier to handle continuous mutations of states.
  • When a complex algorithm is required to handle updating of the app’s states.
  • When a reasonable amount of data changes overtime. If the application has data that is constantly being updated, Redux can be used to manage this data and create a structure for it.
  • When state management is shared. Redux is suitable for cases where the application is shared over a codebase with different developers working on the same application.
How Does Redux Work?
Redux is made up of three primary components: Redux Store, Action, and Reducer.
Redux Store: This is the brain of our state management structure. It manages the state and dispatches Action on managing the state of our application.
Action: refer to instructions sent that can be interpreted by Reducers. It stores information regarding the users’ actions. The action contains a type and a payload. The type is simply a string with the action name while the payload contains the actual data passed by the action.
Reducers: These are functions that read the instructions sent by the Action and update the store via the state according to the received instructions. They specify how the application's state will change in response to the actions sent to the store.
Redux promotes good architectural structure in React apps along with UI management and optimizes performance for easy re-rendering of components when needed.
Redux core benefits include:
  • Flexibility: Redux is simple to learn and implement. It does not require deep knowledge to make use of.
  • Maintainable: Redux is straightforward and easy to maintain and control operations on the stored state.
  • Scalability: Since Redux deals with a central store that contains the state that is shared by components, it makes it suitable for handling states when building complex applications.
  • Redux supports server-side rendering: The current state of an app can be sent to the server with the response to mutate the state.
  • Easy to debug: Since Redux is made up of three core parts: the Store, Actions, and Reducers. By logging the values of these parts it is easy to trace and fix errors if any occur.
We will now look at how to set up and use Redux in an app using a counter application as a case study.
Redux Setup
To set up Redux for use in our application, we will first need to install the package via CLI
npm i redux react-redux
Once this is installed, we can create our action and reducer file; action.js, and reduce.js.
In the reduce.js file, we have:
const reduce = (state = 0, action) => {
  switch (action.type) {
    case "reset value":
      return (state = 0);
    case "increase value":
      return state + 1;
    case "decrese value":
      return state - 1;
    default:
      return state;
  }
};
export default reduce;
Above, we created our reducer file which takes state as value "0" then based on the value of action.type which is the name of the action makes changes to the value of the state. We have set up a conditional block to make different changes based on the type of action dispatched to the reducer. As defined, if the action type is "reset value", state will be set to 0, if the action type is "increase value", the value of state will be increased by one and vice-versa.
Creating a Central Store
To create the store, we will need to create a new file and import createstore from redux:
import { createStore } from "redux";
We also need to import our reducer into the file. After which we will import Provider from Redux. The Provider is responsible for connecting the central state to our application. We will need to wrap the components that will use the Redux state within the provider, preferably in the root.
import reduce from "./reduce"
import { Provider } from "react-redux";
Then to create the store:
const store = createStore(
  reduce,
);
ReactDOM.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>,
  document.getElementById("root")
);
We will have to define our actions and export them in our action.js file:
export const increment = () => {
  return {
    type: "increment value",
  };
};

export const decrement = () => {
  return {
    type: "decrement value",
  };
};

export const reset = () => {
  return {
    type: "reset value",
  };
};
Dispatch Actions
To dispatch the actions increment value, reset value and decrement value we need to import the actions and use the useDispatch hook provided by react-redux to dispatch our actions.
import { useDispatch } from "react-redux";
Then, back in our App.js file, we can create buttons with functionalities to perform the actions we defined:
import { useSelector, useDispatch } from "react-redux";
import {
  decrement,
  increment,
  reset,
} from "./action";

function App() {
  const counter = useSelector((state) => state.counter);
  const dispatch = useDispatch();

  return (
    <div className="App">
      <p>Counter to demonstrate redux</p>
      <h3>{counter}</h3>
      <button onClick={() => dispatch(increment())}>Increment</button>
      <button onClick={() => dispatch(reset())}>Reset</button>
      <button onClick={() => dispatch(decrement())}>Decrement</button>
    </div>
  );
}
export default App;
Here, we have used the useDispatch hook to assign the actions to three different buttons. The value of the counter is based on the current value of the state.
Recoil
What is Recoil?
Recoil is a new state management library that is open-sourced by Facebook. Recoil also provides a solution to the problem of Global state in applications. Recoil is made up of two major parts: Atom and Selector. Atom is simply a function that lets us store states. It is a shared-state architecture where different components can connect to it to get values from the stored state. On the other hand, a Selector is similar to Atom except that it allows us to have a derived state. The value of a Selector can be derived from the Atom or another Selector.
Recoil Setup
To demonstrate how to use Recoil in an application we will use an example similar to the counter we created in Redux above. To install Recoil, run the following command in your CLI:
npm install recoil
Then we can import it into our app and define our state:
//atom.js
import { atom } from "recoil";
const counter = atom({
  key: "counter",
  default: 0
});

export default counterAtom;
Above, the counter is our state. It has a key of "counter" and a default value of 0. In Recoil, components that use recoil state need to be wrapped in RecoilRoot. We will add this to our root component:
//index.js
import React from "react";
import ReactDOM from "react-dom";
import { RecoilRoot } from "recoil";
import App from "./App";

ReactDOM.render(
  <RecoilRoot>
    <App />
  </RecoilRoot>,
  document.getElementById("root")
);
In Recoil, we can use the useRecoilState() hook to read the value of a recoil state. Now we will create buttons with the functionality to mutate the state stored in atom.js:
// App.js
import React from "react";
import { useRecoilState } from "recoil";
import counter from "./atom";

const App = () => {
  const [count, setCount] = useRecoilState(counter);

  return (
    <div>
      <div>
        <button onClick={() => setCount(count + 1)}>Increment</button>
        <span>{count}</span>
        <button onClick={() => setCount(count - 1)}>Decrement</button>
        <button onClick={() => setCount(0)}>Reset</button>
      </div>
    </div>
  );
};

export default App;
Here, we read in the value of our state from atom.js using useRecoilState. We set up three buttons to increase the value of our count, decrement it and reset it.
Benefits of Using Recoil
  • Recoil is a simple way to implement state management. It can be incorporated in applications with simple architectures. Apart from Atom and Selector, there isn't much to it, making it easier to understand.
  • It has a React-like approach as its application is similar to the useState React-hook thereby making it simpler to work within React applications.
Conclusion
So far we've seen how we can manage state in our React applications using various methods and libraries.
Ranging from Hooks and Context API for small to medium projects and Redux and Recoil for medium to large projects.
Still, there are a ton of state libraries to choose from in the React ecosystem and beyond. Most of them are designed and developed around a certain concept and towards a particular use case.
Let's highlight some of these state management libraries:
  • XState - Which is a library built on a concept called state machines. State machines allow you to set a run or perform a specific action for a specific or defined state. A very simple example of a state machine can be seen in a switch case statement
switch (state) {
  case state === 'light':
  // enable light  theme
  break;
  case state === 'dark':
  // enable light theme
  break;
  default:
  // handle error
}
  • You can learn more about XState and state machines from this article, the docs, or watching this course.
  • Mobx - It is a stage-management library that is unopinionated, not platform-specific, which means it can be used outside react. It follows the transparent application of functional reactive programming (TFRP). Unlike some libraries like Redux, MobX allows multiple Stores. You can get started with MobX quite easily as it doesn't have a steep learning curve.
  • Valtio - A simple lightweight proxy-based state management library that provides a mutation-style API
  • Jotai - A Primitive and flexible state management for React which provides a minimalistic API. It supports TypeScript and is optimized for computed values and asynchronous actions to state.
  • Zustand - A small, fast, and scalable state-management solution. It's specifically focused on module state. This library is based on Hooks and is un-opinionated.

The Future of State Management in React

Currently, some of the approaches to state management that we've discussed so far come with some caveats.
For example, in large applications, we might have issues with excessive re-rendering with the Hooks + Context API method.
This is because when Context value changes, every component subscribes to that Context with useContext will re-render even if it doesn't depend on the particular piece of state that was changed.
React has useMemo which is an available workaround for this re-rendering issue. It's a built-in hook that allows you to memoize expensive functions.
In the near future, to fix this issue, the React team has been working on useSelectedContext feature that allows you to subscribe to only a selected part of the Context. You can check out the progress of this feature here.
Although the new useSelectedContext is an in-house solution to this re-rendering problem, other third-party libraries can be used. useContextSelector which is being used under the hood by Jotai and Formik 3, works similarly.
Moving forward, React will automatically figure out which components to re-render using auto-memorization.
All this in addition to the current and purpose-specific options for state management makes React a solid choice for many organizations in developing their products.