Back

Seven Best Libraries for React State Management in 2024

Seven Best Libraries for React State Management in 2024

React state management remains one of the biggest challenges for developers. That is why they are constantly looking for tools that offer much simpler but more efficient processes to handle states. React Hooks are often useful in accessing and sharing states across various components. However, the process becomes complex and overwhelming as the number of components increases. That’s where React state management libraries come in. The availability of various options for such a library can be confusing. To help you pick the best fit for your project, this article discusses top state management libraries, along with their features and how to use them.

State management plays a crucial role in modern, complex React web apps. Solutions from top state management libraries ensure the states in React components are well-organized and continuously updated. You can explore their features and workings given below:

1. Recoil

Recoil Created by Facebook, Recoil is an open-source React state management library with 19.6k stars on GitHub. It utilizes concepts like atoms and selectors to provide an intuitive and declarative way of handling states in React components.

An atom, a shared state unit, represents a single-state property. A component gets its value by subscribing to an atom. Atoms are just like React’s local states, but they have the advantage of being shared across various components.

Selectors, on the other hand, are pure functions. They must subscribe to an atom or another selector to get their value. When the value in the source atom or selector changes, the values in the selectors are recomputed.

Recoil monitors all the atoms and selectors a component uses. So, whenever a connected atom or selector changes its value, Recoil immediately rerenders its component. This simple and flexible approach to state management makes Recoil scalable and performant.

Recoil ensures efficient rerenders and minimal boilerplate. This state management library is easy to maintain and modify.

Key Features

  • Minimal and Reactish: Recoil’s working and approach to state management is similar to React. Adding it to your React app results in a fast, flexible shared state.
  • Data-Flow Graph: Recoil utilizes the combination of efficient subscriptions and pure functions to tame asynchronous queries and derived data.
  • Cross-App Observation: Recoil enables you to observe all the state changes across the app through effective execution of routing, time travel debugging, persistence, or undo.

Installation Documentation - https://recoiljs.org/docs/introduction/installation/

Let’s see how we can use this library in a simple todo list application development. This example demonstrates creating, accessing, and updating the state using the same to-do list functionality. You’ll also find the code explanation below.

With this application we can add and remove todos. The to-do list application will have a title displayed in the list.

import React from 'react';
import { atom, useRecoilState } from 'recoil';

// Define a Recoil atom to hold the todo list state
const todoListState = atom({
  key: 'todoListState',
  default: [],
});

const TodoList = () => {
  const [todoList, setTodoList] = useRecoilState(todoListState); // Use the atom in the component
  const [inputValue, setInputValue] = React.useState('');

  const addTodo = () => {
    setTodoList([...todoList, inputValue]); // Add new todo to the list
    setInputValue(''); // Clear the input field
 };

  const removeTodo = (index) => {
    setTodoList(todoList.filter((_, i) => i !== index)); // Remove todo by index
 };

  return (
    <div>
      <input
        type="text"
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
      />
      <button onClick={addTodo}>Add Todo</button> {/* Add button */}
      <ul>
        {todoList.map((todo, index) => (
          <li key={index}>
            {todo}
            <button onClick={() => removeTodo(index)}>Remove</button> {/* Remove button */}
          </li>
 ))}
      </ul>
    </div>
 );
};

Code Explanation

  • Atom Creation: Components subscribe to the units of state called Atom. We can define the todoListState atom using a unique key and a default value of an empty array.
  • State Hook: You can access the current value of the todoList using the useRecoilState hook and update it with the setTodoList function.
  • Adding Todos: The current input value is added to the todoList using the addTodo function. It also reset the input field after the update.
  • Removing Todos: You can filter out todos with the removeTodo function depending on its index. It also updates the todoList.
  • Rendering: An input field, a button for adding todos, and a list displaying all current todos with a remove button are rendered by the component.

2. Redux

Redux Initially released in 2015, Redux is the first React state management library. Its purpose is to simplify state management. Redux does so by offering a centralized state repository, a single store for handling all the states in your app.

Actions and reducers manage the states for Redux. Actions inform the store about events that should take place. At the same time, reducers are the functions implemented on the basis of an object’s initial state and input from the action. You can modify the component’s state by dispatching an action to the reducer. It changes the state and stores it in the central repository.

Using Redux Toolkit helps developers reduce the boilerplate and simplify the code. Its pure functions and simple logic make it easy to use, test, and maintain. Redux has 60.8k stars on GitHub, boasting large community support with a rich ecosystem of tools and add-ons.

Key Features

  • Predictable: Redux enables you to create apps with consistent behavior. You can easily test and run them in different environments.
  • Centralized: This library offers robust state management capabilities such as state persistence and undo/redo by centralizing the logic and states of your application.
  • Debuggable: Redux DevTools allows you to track and log changes in your app’s states. On top of that, it also provides time-travel debugging and detailed error reports.
  • Flexible: The Redux library is highly compatible. It allows you to use the UI layer of your choice, and its large ecosystem offers various add-ons that can fulfill your requirements.

Installation Documentation - https://redux.js.org/introduction/installation.

Let’s see how we can use this library with the same to-do list application development. You’ll also find its explanation below.

import React from "react";
import { createStore } from "redux";
import { Provider, useSelector, useDispatch } from "react-redux";

// Initial state for our Redux store
const initialState = { todos: [] };

// Reducer function to handle actions
const reducer = (state = initialState, action) => {
  switch (action.type) {
    case "ADD_TODO":
      return { todos: [...state.todos, action.payload] }; // Add a new todo
    case "REMOVE_TODO":
      return {
        todos: state.todos.filter((_, index) => index !== action.payload),
 }; // Remove todo by index
    default:
      return state; // Return current state if no action matches
 }
};

// Create Redux store
const store = createStore(reducer);

const TodoList = () => {
  const todos = useSelector((state) => state.todos); // Access todos from state
  const dispatch = useDispatch(); // Function to dispatch actions
  const [inputValue, setInputValue] = React.useState("");

  const addTodo = () => {
    dispatch({ type: "ADD_TODO", payload: inputValue }); // Dispatch action to add todo
    setInputValue(""); // Clear the input field
 };

  return (
    <div>
      <input
        type="text"
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
      />
      <button onClick={addTodo}>Add Todo</button> {/* Add button */}
      <ul>
        {todos.map((todo, index) => (
          <li key={index}>
            {todo}
            <button
              onClick={() => dispatch({ type: "REMOVE_TODO", payload: index })}
            >
 Remove
            </button>{" "}
            {/* Remove button */}
          </li>
 ))}
      </ul>
    </div>
 );
};

// Wrap your app with Provider to access the store
const App = () => (
  <Provider store={store}>
    <TodoList />
  </Provider>
);

Code Explanation

  • Initial State and Reducer: The initialState object comprises an empty array of todos and the reducer function manages “ADD_TODO” and “REMOVE_TODO” actions. Based on the dispatched actions, it can also determine how the state changes.
  • Store Creation: Use the createStore function to build a Redux store and then add a reducer for state management.
  • Accessing State: The useSelector hook is used to fetch the current todos list from the state of a Redux store
  • Dispatching Actions: Actions are dispatched using a method from the useDispatch hook. In the above code, it is used to dispatch actions like adding and removing todos.
  • User Interface: It is a component providing an input field, a button to add new todos, and a list showing all the todos with a remove button.

3. Zustand

Zustand Zustand takes a minimalistic approach to state management in React applications. With a size under 1kb, Zustand is the smallest state management library. Its lightweight nature helps Zustand to become fast and scalable.

It comes with well-known APIs, inspired by Context APIs and hooks, that help create and maintain a centralized state repository. This single store simplifies access and updates from different React components. Zustand offers clean and readable code. It renders components only when their state changes.

Zustand is the only React state management library that claims to have overcome the issues of context loss between renderers, React concurrency, and zombie child problems. This library can also handle transient state updates without rerendering the components.

With over 46.7k stars on GitHub, Zustand is an ideal option for projects that require basic state management and not the complexities that come with flux-based libraries.

Key Features

  • Declarative and Reactive: You can now clearly and concisely define state slices and state updates, thanks to Zustand’s declarative approach. Understanding the app’s state becomes easy with that.
  • Modular and Encapsulated: By enabling you to define data slices independently, Zustand encourages encapsulation and modularity in state management. This helps maintain code reusability and manage complex states.
  • Hook-Based: Zustand utilizes React Hooks to offer intuitive and seamless state management. This approach also sits well with modern React development practices.
  • React Concurrent Mode Support: Zustand was designed to be versatile so it can perform well in scenarios requiring concurrent rerendering. This allows you to manage the complex and dynamic UIs efficiently.

Installation Documentation - https://www.npmjs.com/package/zustand

Let’s see how we can use this library with the same to-do list application development. You’ll also find its explanation below.

import create from "zustand";

// Create a Zustand store for managing todo state
const useStore = create((set) => ({
  todos: [],
  addTodo: (todo) => set((state) => ({ todos: [...state.todos, todo] })), // Function to add todo
  removeTodo: (index) =>
    set((state) => ({ todos: state.todos.filter((_, i) => i !== index) })), // Function to remove todo by index
}));

const TodoList = () => {
  const { todos, addTodo, removeTodo } = useStore(); // Use Zustand store
  const [inputValue, setInputValue] = React.useState("");

  const handleAddTodo = () => {
    addTodo(inputValue); // Call add function from store
    setInputValue(""); // Clear input field
 };

  return (
    <div>
      <input
        type="text"
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
      />
      <button onClick={handleAddTodo}>Add Todo</button> {/* Add button */}
      <ul>
        {todos.map((todo, index) => (
          <li key={index}>
            {todo}
            <button onClick={() => removeTodo(index)}>Remove</button>{" "}
            {/* Remove button */}
          </li>
 ))}
      </ul>
    </div>
 );
};

Code Explanation

  • Store Definition: Create a Zustand store with a create function. It starts with an empty todos array and manages it using addTodo and removeTodo methods.
  • Using the Store: The current list of todos in the TodoList is accessed using destructuring. It also requires other functions to add and remove items from that list.
  • Adding and Removing Todos: Clicking the buttons rendered on the user interface helps the component invoke various functions, such as adding or removing the todos.

4. MobX

MobX MobX is one of the most popular state managers available in the market. It follows the OOP paradigm and handles the state using an observer/observables pattern. MobX builds an observable data model that can act as a reference for your observers and components. Then, it tracks the data accessed by components and renders them when the data changes.

MobX is immutable as it enables you to update the state by building new state objects instead of mutating existing ones. This helps avoid side effects in your app. Taking a reactive approach, MobX also encourages you to write concise and reusable code.

This state management library also ensures better usability, modifiability, scalability, and performance. With over 27.5k stars on GitHub, MobX has great community support and a rich ecosystem.

Key Features

  • Straightforward: MobX helps you write boilerplate-free and minimalistic code that gives a sense of your purpose. It doesn’t need special tools to update data in an asynchronous process. Its reactivity system can detect and transmit all the changes to where they are used.
  • Effortless optimal rendering: It tracks every single use and changes in your data at runtime. This helps with the creation of a dependency tree showing all the relations between state and output. MobX runs computations depending on the state only when it is required. And you no longer need to optimize the components manually.
  • Architectural freedom: You can use any UI framework to manage the app state because MobX is not opinionated. Due to this, your code becomes easy to test, portable, and decoupled.

Installation Documentation - https://mobx.js.org/installation.html

Let’s see how we can use this library with the same to-do list application development. You’ll also find its explanation below.

import React from "react";
import { makeAutoObservable } from "mobx";
import { observer } from "mobx-react-lite";

// Define a MobX store for managing todos
class TodoStore {
  todos = [];

  constructor() {
    makeAutoObservable(this); // Automatically observe properties and methods
 }

  addTodo(todo) {
    this.todos.push(todo); // Add a new todo
 }

  removeTodo(index) {
    this.todos.splice(index, 1); // Remove todo by index
 }
}

const todoStore = new TodoStore();

const TodoList = observer(() => {
  const [inputValue, setInputValue] = React.useState("");

  const handleAddTodo = () => {
    todoStore.addTodo(inputValue); // Call add method from store
    setInputValue(""); // Clear input field
 };

  return (
    <div>
      <input
        type="text"
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
      />
      <button onClick={handleAddTodo}>Add Todo</button> {/* Add button */}
      <ul>
        {todoStore.todos.map((todo, index) => (
          <li key={index}>
            {todo}
            <button onClick={() => todoStore.removeTodo(index)}>
 Remove
            </button>{" "}
            {/* Remove button */}
          </li>
 ))}
      </ul>
    </div>
 );
});

Code Explanation

  • Store Creation: The TodoStore class offers observable properties and methods. Hence, the changes in observables are automatically tracked by the constructor with its makeAutoObservable.
  • Adding/Removing Todos: You don’t need any additional dispatch or action creators to modify the observable todos array. The methods to add and remove todos are enough for the job.
  • Observer Component: The observer wraps the TodoList component, which is rerendered every time observables see a change.

5. Jotai

Jotai Introduced in 2021, Jotai adopts an atomic approach to React state management. It handles everything like an atom and offers a simple API to create and access atoms across your application. Jotai combines the atoms to create states and optimizes the renders automatically based on their atom dependency.

Jotai is popular for its clean API and minimal footprint, thanks to its optimized memory usage and automatic garbage collection. Being compatible with React’s new concurrent mode, Jotai is also optimized for performance. This makes it an ideal choice for complex and high-performance applications. It has 18.4k stars on GitHub.

Key Features

  • Minimal Core API: Jotai takes a minimalist approach because it has a minimal API weighing only 2kb. That is why the Jotai bundle exposes fewer exports. These exports are classified into four categories namely atom, useAtom, Store, and Provider.
  • Rich Ecosystem: Jotai offers a large variety of utilities, official extensions, plugins, and DevTools. They help simplify the state management process and provide an enhanced developer experience.
  • Detailed Documentation: It provides step-by-step guides to implement common concepts and advanced patterns of the state management library across different frameworks.

Installation Documentation - https://www.npmjs.com/package/jotai

Let’s see how we can use this library with the same to-do list application development. You’ll also find its explanation below.

import React from "react";
import { atom, useAtom } from "jotai";

// Create an atom for managing the todo list state
const todoListAtom = atom([]);

const TodoList = () => {
  const [todos, setTodos] = useAtom(todoListAtom); // Use Jotai atom in the component
  const [inputValue, setInputValue] = React.useState("");

  const addTodo = () => {
    setTodos([...todos, inputValue]); // Add new todo to the list
    setInputValue(""); // Clear input field
 };

  const removeTodo = (index) => {
    setTodos(todos.filter((_, i) => i !== index)); // Remove todo by index
 };

  return (
    <div>
      <input
        type="text"
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
      />
      <button onClick={addTodo}>Add Todo</button> {/* Add button */}
      <ul>
        {todos.map((todo, index) => (
          <li key={index}>
            {todo}
            <button onClick={() => removeTodo(index)}>Remove</button>{" "}
            {/* Remove button */}
          </li>
 ))}
      </ul>
    </div>
 );
};

Code Explanation

  • Atom Definition: The atom function from Jotai helps create the todoListAtom starting with an empty array.
  • Using Atoms: Calling the useAtom in a component allows you to access both the todos current value and a setter function (setTodos) to update it.
  • State Management: Adding and removing todos are managed by defining functions. The rerenders are triggered when the atom’s state is modified, but only where necessary.

6. SWR (Stale-While-Revalidate)

SWR SWR library is specially designed for asynchronous development projects. It makes the asynchronous tasks easy for developers. SWR reduces the network requests to optimize performance. This ought to offer a smooth developer experience.

As its name Stale-While-Revalidate suggests, the library serves the user-cached data while simultaneously sending a revalidation request to fetch updated data. SWR is also flexible, allowing you to easily fetch complex data in React. Every time a client browser makes a GET request, SWR checks whether the same request has been made before. If so, it serves data from the cached memory to the user.

However, in the background, SWR will also request to check if the data has been updated since the last time it was fetched from the server. If so, then the library will get the new updated data, cache it, and serve it to the user. It is important to note that SWR has 30.3k stars on GitHub.

Key Features

  • Stale-While-Revalidate strategy: SWR provides cached data and simultaneously requests a revalidation in the background. This helps you serve data quickly.
  • Automatic caching: The SWR library provides a default feature for caching that automatically saves the requested data. So, when another request is made for the same data, it can be instantly served from the cache.
  • Automatic revalidation: You don’t have to revalidate the stale data manually. Once you set the conditions like onHover, onMount, and onFocus, SWR will do it for you.
  • Dependency tracking: SWR tracks every data fetching function in your components for its dependencies. Any change in variables would lead to automatic data revalidation. So, you don’t need to use the React useEffect Hook.

Installation Documentation - https://swr.vercel.app/docs/getting-started

Let’s see how we can use this library with the same to-do list application development. You’ll also find its explanation below.

import React from "react";
import useSWR, { mutate } from "swr";

const fetcher = (url) => fetch(url).then((res) => res.json());

const TodoList = () => {
  const { data: todos = [], mutate } = useSWR("/api/todos", fetcher); // Fetch todos from an API

  const addTodo = async (todo) => {
    await fetch("/api/todos", {
      method: "POST",
      body: JSON.stringify({ title: todo }), // Send a POST request to add a new todo
 });
    mutate(); // Re-fetch after adding
 };

  const removeTodo = async (index) => {
    await fetch(`/api/todos/${index}`, { method: "DELETE" }); // Send a DELETE request to remove the todo
    mutate(); // Re-fetch after removing
 };

  return (
    <div>
      {/* Input and Add Todo button would go here */}
      <ul>
        {todos.map((todo, index) => (
          <li key={index}>
            {todo.title}
            <button onClick={() => removeTodo(index)}>Remove</button>{" "}
            {/* Remove button */}
          </li>
 ))}
      </ul>
    </div>
 );
};

Code Explanation

  • Data Fetching: You can retrieve data from an API using a fetcher function. Every time there is a change in data, SWR automatically takes care of caching, revalidation, and updates.
  • Automatic Caching & Revalidation: When todos are added or removed, you can trigger data re-fetching through the mutate function. It is to keep your user interface up-to-date with the server data.
  • Handling API Calls: You need to send requests to specific API endpoints to add and remove todos. The mutate() function is called after finishing these actions.

7. Rematch

Rematch Rematch brings a pragmatic approach to React state management. It uses the same concepts as Redux but with a smaller footprint and simpler APIs. Rematch is a lightweight framework with a bundle size of just 1.7kb. But it can still minimize the boilerplate and simplify the setup.

At its core, Rematch works with models to handle states, effects, and reducers in a single entity. It even borrows Redux’s best practices for better and simpler state management. Learning to use Rematch is easy, and its clean interface attracts most developers.

Rematch has 8.5k stars on GitHub. It is written in TypeScript and comes with a variety of plugins.

Key Features

  • No configuration: Rematch manages everything from a single file. So, you don’t need action creators, action types, switch statements, and thunks anymore.
  • Framework agnostic: You can use Rematch in any browser or framework you want, be it React, Angular, or Vue.
  • Plugins API: You get a simple API interface in Rematch, allowing you to design custom plugins that help extend the functionalities of this library. The official plugin offerings are also out-of-the-box.
  • TypeScript support: Thanks to its TypeScript support, Rematch can autocomplete all the states, methods, and reducers.

Installation Documentation - https://rematchjs.org/docs/getting-started/installation/

Let’s see how we can use this library with the same to-do list application development. You’ll also find its explanation below.

import React from "react";
import { init } from "@rematch/core";

// Define a model for managing todos in Rematch
const model = {
  state: { todos: [] },
  reducers: {
    addTodo(state, todo) {
      return { ...state, todos: [...state.todos, todo] }; // Add new todo to state
 },
    removeTodo(state, index) {
      return { ...state, todos: state.todos.filter((_, i) => i !== index) }; // Remove todo by index
 },
 },
};

// Initialize Rematch store with models
const store = init({
  models: { todos: model },
});

const TodoList = () => {
  const todos = store.getState().todos.todos; // Access current todos

  const addTodo = (todo) => store.dispatch.todos.addTodo(todo); // Dispatch action to add todo
  const removeTodo = (index) => store.dispatch.todos.removeTodo(index); // Dispatch action to remove todo

  const [inputValue, setInputValue] = React.useState("");

  return (
    <div>
      <input
        type="text"
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
      />
      <button
        onClick={() => {
          addTodo(inputValue);
          setInputValue("");
 }}
      >
 Add Todo
      </button>{" "}
      {/* Add button */}
      <ul>
        {todos.map((todo, index) => (
          <li key={index}>
            {todo}
            <button onClick={() => removeTodo(index)}>Remove</button>{" "}
            {/* Remove button */}
          </li>
 ))}
      </ul>
    </div>
 );
};

// Wrap your app with Provider if needed (not shown)
const App = () => (
  <Provider store={store}>
    <TodoList />
  </Provider>
);

Code explanation

  • Model Definition: Rematch’s models define both initial state and reducers. The model handles an empty array of todos.
  • Store Initialization: Models that define how states are updated are initialized along with the Rematch store.
  • Accessing State & Dispatching Actions: The component leverages the store.getState() to access current todos and dispatched actions for updates.

Conclusion

These were the top React state management libraries available in the market. After looking at their features and how to use them, you might have identified suitable options for your project.

However, you must also consider your requirements and compare the available options against factors such as desired performance characteristics, app complexity, and team familiarity. That ought to help you pick the right fit.

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay