Frontend Development 36 min read

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.

Sohu Tech Products
Sohu Tech Products
Sohu Tech Products
Comprehensive Guide to State Management in React

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.

state managementreactHooksZustandContext APIForm HandlingReact QuerySWR
Sohu Tech Products
Written by

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.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.