State Management in React Apps
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-persisted | Client state is temporary and is only available during the current session, or is stored in local storage. |
Synchronous | Changes to client state happen immediately and synchronously. |
Client owned | The client has full control and ownership over the client state. |
Reliably up to date | Client 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 persisted | Server state is stored and persisted on remote servers, allowing access across different sessions and devices. |
Asynchronous | Fetching server state requires asynchronous operations due to network latency and remote data retrieval. |
Shared ownership | Multiple clients can access and modify the same server state. |
Potentially stale | Server 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
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;
};
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>;
};
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
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.
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:
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:
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.