Handbook
State Management

State Management in React Apps

Last updated by Noel Varanda (opens in a new tab),
React
State Management
Server State Management
Client State Management
React Query
React Context
Redux
Zustand
Jotai
Valtio

Apps are just opinionated ways of displaying and manipulating data.

Watch React Query's creator Tanner Linsley's It's Time to Break up with your "Global State"! (opens in a new tab).

By understanding the fundamental differences between client and server state, we can make better decisions about how to manage state in our apps.

Preview and sample code

If you want a minimal react app setup with all state management examples then have a look at all the code here (opens in a new tab).

Quick note on local state

Local state (opens in a new tab) refers to managing state within a specific component without the need for a state management solutions. You can use React's built-in useState hook to handle local state.

Here are some situations where local state is appropriate:

  • When the state is relevant only to a specific component and doesn't need to be shared.
  • For straightforward state needs that don't require complex interactions with other parts of the application.

The rest of this article will focus on managing state that is shared across multiple components, which is where state management solutions come in.

A brief history of global state

When React first came out, it came with the problem of prop drilling: passing props down through multiple levels of components. This approach was tedious and error-prone, so developers began to look for better solutions.

Enter Redux, a state management library that provided a centralized location for storing and managing application state. Redux was a game-changer, and it quickly became the go-to solution for managing state in React applications.

const globalState = {
  theme: 'dark',
  toasts: [],
  alerts: [],
  // ... other client state properties
  products: [],
  categories: [],
  // ... other server state properties
};

Redux introduced the concept of global state. This meant:

  • We could avoid prop drilling.
  • State could be accessed from anywhere in the app.
  • State could be easily shared between components.

However, as applications grew in size and complexity, developers began to realize that Redux was not a one-size-fits-all solution. Global state tried to treat app state and server state the same way, even though they face very different challenges.

Client and server state

Global state is an outdated concept. Instead, we should think about client state and server state. These two types of states have distinct characteristics and pose different challenges for state management, such as:

  • Storage location
  • Speed of access
  • Access methods
  • Ownership and modification
  • Data freshness

Client state

Client state refers to non- or locally-persisted data that is specific to the client-side application. It is characterized by the following attributes:

Non- or locally-persistedClient state is temporary and is only available during the current session, or is stored in local storage.
SynchronousChanges to client state happen immediately and synchronously.
Client ownedThe client has full control and ownership over the client state.
Reliably up to dateClient state is always up to date as changes occur in real-time.

Examples of client state include:

const clientState = {
  theme: 'dark',
  toasts: [],
  alerts: [],
  user: null,
  cart: [],
  formWizardValues: {},
  // ... other client state properties
};

Server state

Server state, on the other hand, refers to data that is fetched from remote servers and may be shared among multiple clients. It exhibits the following characteristics:

Remotely persistedServer state is stored and persisted on remote servers, allowing access across different sessions and devices.
AsynchronousFetching server state requires asynchronous operations due to network latency and remote data retrieval.
Shared ownershipMultiple clients can access and modify the same server state.
Potentially staleServer state may not always reflect the most recent changes, as it could be affected by caching or delayed updates.

Examples of server state include:

const serverState = {
  products: [],
  categories: [],
  users: [],
  chat: [],
  collaboration: {},
  artciles: [],
  // ... other server state properties
};

Solving server state

Read the data fetching guide to understand how to solve server state.

Solving client state

When it comes to managing client state in React applications, Zustand emerges as a powerful and efficient solution. By leveraging Zustand, we can overcome the challenges associated with client state management while embracing simplicity and scalability.

Check out the npm trends for some of the newer client state libraries (opens in a new tab) mentioned in this article.

The reduction of problems

With the separation of global state into client and server state, the complexity of client state management diminishes. By focusing on the remaining client state concerns, we can address common issues such as theme management, toasts, alerts, user data, cart items, and form values.

const clientState = {
  theme: 'dark',
  toasts: [],
  alerts: [],
  user: null,
  cart: [],
  formWizardValues: {},
  // ... other client state properties
};

Favourites

  • Zustand: For small to medium sized apps.
  • Redux: For large, complex apps.

Solutions

⚠️

The following list presents some available solutions for client state management, but it is not exhaustive.

There are quite a few solutions for managing client state in React applications:

Native

React Context (opens in a new tab) is a native, built-in feature of React. It allows for global state management within a component tree without the need for additional libraries or dependencies.

Example: Theme management with React context
@/providers/ThemeContext.tsx
import React, { createContext, useContext } from 'react';
 
interface ThemeState {
  theme: 'light' | 'dark';
  toggleTheme: () => void;
}
 
const ThemeContext = createContext<ThemeState | undefined>(undefined);
 
export const ThemeProvider: React.FC = ({ children }) => {
  const toggleTheme = () => {
    // Retrieve the current theme from the context
    setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light'));
  };
 
  const [theme, setTheme] = React.useState<'light' | 'dark'>('light');
 
  // Wrap the children components with the ThemeContext provider
  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
};
 
// Custom hook to access the theme state from any component
export const useTheme = (): ThemeState => {
  const themeContext = useContext(ThemeContext);
  if (!themeContext) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return themeContext;
};
@/layouts/main/Navbar.tsx
import React from 'react';
import { useTheme } from '../store/theme';
 
export const Navbar: React.FC = () => {
  const { theme } = useTheme();
 
  return <div>You're currently using the {theme} mode.</div>;
};
@/App.tsx
import React from 'react';
import { ThemeProvider } from './store/theme';
import { Navbar } from './layouts/main/Navbar';
 
export const App: React.FC = () => (
  <ThemeProvider>
    <Navbar />
  </ThemeProvider>
);

Though native, implementing a state management system using only React Context can be quite a pitfall as many of the state management libraries out there come with performance benefits, dev tooling, middleware support, and other useful features.

Pros

  • Simple API, built into React.
  • Lightweight, no additional dependencies.
  • Good for small-scale applications or simple state sharing.

Cons

  • Performance is very dependant on implementation.

  • Re-inventing the wheel.

  • Lack of built-in middleware support limits asynchronous tasks.

  • No built-in middleware or DevTools support.

Traditional

Traditional state management libraries have been widely used and established in the React ecosystem for managing complex global state. They provide advanced features, extensive tooling, and have large and mature ecosystems.

Redux

Redux (opens in a new tab) stands out as a well-equipped state management library. It has been the most popular library in use, especially in the time where managing state globally was a more common paradigm than dividing client- from server-state. It provides a single source of truth with a centralized storre.

Pros

  • Advanced state management capabilities.
  • Large ecosystem, extensive tooling support.
  • Middleware support for handling complex asynchronous actions.

Cons

  • Steeper learning curve, more boilerplate code.

  • Requires a lot of set up.

  • Overkill for small-scale applications.

MobX

MobX (opens in a new tab) is a simple library which uses observable objects to track changes and automatically updates components that depend on the observed data.

Lightweight

These libraries provide lightweight alternatives to the traditional, heavier state management systems, providing a minimalist approach and simpler APIs.

Zustand

Zustand (opens in a new tab) is a lightweight state management library with a focus on simplicity, performance, and a small footprint. It offers a minimalistic API for managing state in a more straightforward manner compared to traditional libraries like Redux. Zustand also offers seamless integration with React Query or Apollo Client, enabling efficient management of server state while efficiently handling the client state.

Example: Theme Management with Zustand
src/store/theme.ts
import { create } from 'zustand';
 
interface ThemeState {
  theme: 'light' | 'dark';
  toggleTheme: () => void;
}
 
export const useThemeStore = create<ThemeState>((set, get) => ({
  theme: 'light',
  toggleTheme: () => {
    set({
      theme: get().theme === 'light' ? 'dark' : 'light',
    });
  },
}));

We can use the persist middleware to persist the theme state in local storage.

src/store/theme.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
 
interface ThemeState {
  theme: 'light' | 'dark';
  toggleTheme: () => void;
}
 
export const useThemeStore = create(
  persist<ThemeState>(
    (set, get) => ({
      theme: 'light',
      toggleTheme: () => {
        set({
          theme: get().theme === 'light' ? 'dark' : 'light',
        });
      },
    }),
    { name: 'theme' }
  )
);

To consume the theme state:

src/layouts/main/Navbar.tsx
const Navbar = () => {
  const theme = useThemeStore((store) => store.theme);
 
  return <div>You're currently using the {theme} mode.</div>;
};

Pros

  • Simple API, minimal boilerplate code.
  • Excellent performance and scalability.

  • Seamless integration with React Query or Apollo Client.

Cons

  • Smaller ecosystem compared to Redux.

  • May not be suitable for complex state management scenarios.

Atomic

Atomic libraries introduce the concept of atoms and selectors, which provide a more granular and efficient approach to managing state. They allow for more fine-grained control and offer optimized reactivity by tracking dependencies between atoms and selectors.

"An atom represents a piece of state that you can read and update anywhere in your application. Think of it as a useState that can be shared in any component." - Tom Lienard's "An introduction to atomic state management libraries in React (opens in a new tab)"

Recoil

Recoil (opens in a new tab) was developed by Facebook specifically for React applications. It introduces atoms, selectors, and a React hook-based API. Recoil emphasizes simplicity.

Example: Theme Management with Recoil

For a fully-fledged example with local storage read Konstantin Tarkus's How to implement UI theme switching (dark mode) with React and Recoil (opens in a new tab)

Here's an example of managing theme state with Recoil:

src/store/theme.ts
import { atom, useRecoilState } from 'recoil';
 
const themeState = atom({
  key: 'theme',
  default: 'light',
});
 
export const ThemeSwitcher = () => {
  const [theme, setTheme] = useRecoilState(themeState);
 
  const toggleTheme = () => {
    setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light'));
  };
 
  return (
    <div>
      <span>Current theme: {theme}</span>
      <button onClick={toggleTheme}>Toggle theme</button>
    </div>
  );
};

In this example, the themeState atom defines the theme state, and the useRecoilState hook is used to consume and update the theme state. Components subscribed to the theme state atom will automatically re-render when the theme state changes.

Pros

  • Simple API, minimal boilerplate code.
  • Developed by Facebook specifically for React applications.
  • Integrates well with React's component model.

Cons

  • Limited abstractions compared to more feature-rich solutions.
  • Relatively new library, which may result in a smaller community and ecosystem.

Jotai

Jotai (opens in a new tab) is another atomic state management library that uses React hooks. It allows you to create atom-based state without the need for a centralized store. Jotai provides a scalable and flexible solution for managing state.

Pros

  • Simple API, minimal boilerplate code.
  • Lightweight and flexible state management.
  • Works well with React hooks.

Cons

  • Limited abstractions compared to more feature-rich solutions.
  • Smaller community and ecosystem compared to more established libraries.

Proxies

Proxies are objects that intercept and customize fundamental operations, enabling reactive behavior. In Valtio, proxies are used to create reactive state objects that automatically trigger updates when accessed or modified. Proxies simplify state management by automatically tracking dependencies and enabling reactivity without the need for explicit event handling, improving performance by minimizing unnecessary re-renders and ensuring precise updates.

Valtio

Valtio (opens in a new tab) is a minimalist state management library that uses proxies to create reactive state objects. It provides a simple API for creating and accessing state, making it ideal for small to medium-sized apps.

Pros

  • Simple API, minimal boilerplate code.
  • Lightweight and suitable for small to medium-sized apps.

Cons

  • Limited information available on pros and cons specific to Valtio.

Keep up to date with any latest changes or announcements by subscribing to the newsletter below.