Comprehensive Guide to State Management in React
This guide explains React’s various state types—local, class, global, navigation, form, and persistent—showing how to manage each with built‑in hooks, Context, libraries such as Zustand, Jotai, React Query, SWR, React Router, TanStack Router, React Hook Form, Formik, and persistence tools like localStorage and redux‑persist.
React is the most popular front‑end framework, and state management is crucial for building complex, interactive applications. This guide covers the different types of state in React—local, global, server, navigation, and form state—and presents a variety of management solutions with code examples.
Local State
Local state lives inside a single component. It is private, scoped to the component and its children, and follows the component lifecycle.
For function components, the useState hook declares state and returns a value and an updater function. Example:
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
Current count: {count}
setCount(count + 1)}>Click me
);
}When state updates depend on the previous value, useReducer provides a reducer pattern similar to Redux but scoped to a component.
import React, { useReducer } from 'react';
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case 'increment': return { count: state.count + 1 };
case 'decrement': return { count: state.count - 1 };
default: throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
Count: {state.count}
dispatch({ type: 'increment' })}>+
dispatch({ type: 'decrement' })}>-
);
}Custom hooks can encapsulate reusable local‑state logic.
import { useState, useEffect } from 'react';
import axios from 'axios';
export function useFetchData(url, params) {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await axios.get(url, { params });
setData(response.data);
} catch (err) {
setError(err.message);
} finally {
setIsLoading(false);
}
};
fetchData();
}, [url, params]);
return { data, isLoading, error };
}Class Component State
Before hooks, class components managed state via the state object and this.setState method.
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
increment = () => {
this.setState(prev => ({ count: prev.count + 1 }));
};
render() {
return (
Current count: {this.state.count}
Click me
);
}
}Global State
Global state is shared across many components. Options include lifting state, the Context API, and third‑party libraries such as Zustand, Jotai, React Query, and SWR.
Context API
Create a context and provide it at the top level; consumers retrieve it with useContext .
import React, { createContext, useState } from 'react';
const MyContext = createContext();
function App() {
const [globalState, setGlobalState] = useState({ count: 0 });
return (
);
}Zustand
Zustand offers a minimal API for global stores.
import { create } from 'zustand';
const useStore = create(set => ({
count: 0,
increment: () => set(state => ({ count: state.count + 1 })),
decrement: () => set(state => ({ count: state.count - 1 })),
}));Jotai
Jotai uses atoms as primitive state units.
import { atom } from 'jotai';
export const countAtom = atom(0);
export const doubleCountAtom = atom(get => get(countAtom) * 2);
function Counter() {
const [count, setCount] = useAtom(countAtom);
const doubleCount = useAtomValue(doubleCountAtom);
return (
Count: {count}
Double: {doubleCount}
setCount(c => c + 1)}>Increment
);
}React Query (TanStack Query)
React Query handles data fetching, caching, and mutations with hooks like useQuery and useMutation .
import { useQuery, useMutation, QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { getTodos, postTodo } from '../my-api';
const queryClient = new QueryClient();
function App() {
return (
);
}
function Todos() {
const { data, error, isLoading } = useQuery(['todos'], getTodos);
const mutation = useMutation(postTodo, {
onSuccess: () => queryClient.invalidateQueries(['todos']),
});
// render list and a button that calls mutation.mutate(...)
}SWR
SWR follows the stale‑while‑revalidate pattern, returning cached data instantly and re‑fetching in the background.
import useSWR from 'swr';
import axios from 'axios';
const fetcher = url => axios.get(url).then(res => res.data);
function BlogPosts() {
const { data, error, isValidating } = useSWR('/api/posts', fetcher, { retry: 3, revalidateOnFocus: false });
if (error) return
Load error: {error.message}
;
if (!data) return
Loading... {isValidating && 'revalidating...'}
;
return (
Blog Posts
{data.map(post => (
{post.title}
{post.content}
Published: {new Date(post.date).toLocaleDateString()}
))}
);
}Navigation State
React Router and TanStack Router manage route‑based state, URL parameters, query strings, and navigation‑time state objects.
React Router
import { BrowserRouter as Router, Routes, Route, Link, useParams, useLocation, useNavigate } from 'react-router-dom';
function App() {
return (
} />
);
}
function UserDetail() {
const { userId } = useParams();
return
User ID: {userId}
;
}
// Navigation with state
function ProductList() {
const navigate = useNavigate();
const addToCart = id => navigate('/cart', { state: { productId: id } });
return
addToCart('123')}>Add to cart
;
}
function Cart() {
const { state } = useLocation();
return
{state ? `Product ID: ${state.productId}` : 'Cart is empty'}
;
}TanStack Router
import { createRouter, RouterProvider, useParams, useSearchParams } from '@tanstack/react-router';
const router = createRouter({
routes: [
{ path: '/products/:id', component: ProductDetail },
{ path: '/search', component: SearchResults },
],
});
function ProductDetail() {
const { id } = useParams();
return
Product ID: {id}
;
}
function SearchResults() {
const [searchParams] = useSearchParams();
const q = searchParams.get('q') || '';
const sort = searchParams.get('sort') || '';
return
Search: {q}, Sort: {sort}
;
}Form State
Forms can be handled with plain useState , or with libraries such as React Hook Form and Formik for validation and performance.
useState
function LoginForm() {
const [form, setForm] = useState({ username: '', password: '' });
const handleChange = e => {
const { name, value } = e.target;
setForm(prev => ({ ...prev, [name]: value }));
};
const handleSubmit = e => {
e.preventDefault();
console.log('Form data', form);
};
return (
Username:
Password:
Login
);
}React Hook Form
import { useForm } from 'react-hook-form';
function LoginForm() {
const { register, handleSubmit, formState: { errors } } = useForm();
const onSubmit = data => console.log(data);
return (
Username:
{errors.username &&
{errors.username.message}
}
Password:
{errors.password &&
{errors.password.message}
}
Login
);
}Formik
import { Formik, Form, Field, ErrorMessage } from 'formik';
import * as Yup from 'yup';
const schema = Yup.object({
username: Yup.string().required('Username required'),
password: Yup.string().required('Password required'),
});
function LoginForm() {
return (
{
console.log(values);
actions.setSubmitting(false);
}}
>
{({ isSubmitting }) => (
Username:
Password:
Login
)}
);
}Persistent State
Persisting state across page reloads can be achieved with Web Storage, Cookies, IndexedDB, or by adding persistence middleware to state‑management libraries.
Web Storage (localStorage / sessionStorage)
function PersistentForm() {
const [data, setData] = useState(() => JSON.parse(sessionStorage.getItem('formData')) || {});
useEffect(() => {
sessionStorage.setItem('formData', JSON.stringify(data));
}, [data]);
// form implementation omitted for brevity
}Zustand with persist middleware
import create from 'zustand';
import { persist } from '@zustand/middleware';
const useStore = create(persist(set => ({
count: 0,
increment: () => set(state => ({ count: state.count + 1 })),
decrement: () => set(state => ({ count: state.count - 1 })),
}), { key: 'myStore', storage: localStorage }));Redux with redux-persist
import { createStore } from 'redux';
import { persistStore, persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import rootReducer from './reducers';
const persistConfig = { key: 'root', storage };
const persistedReducer = persistReducer(persistConfig, rootReducer);
export const store = createStore(persistedReducer);
export const persistor = persistStore(store);Integrate the persistor with PersistGate in the React tree to rehydrate the state on app start.
Sohu Tech Products
A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.