skip to content
logo
Table of Contents

Advanced React State Management

Managing state in large React applications can be challenging.
This guide covers:

  • When to use local state vs global state
  • React Context and useReducer
  • Optimizing performance with selectors and memoization
  • Using lightweight state libraries like Zustand

1. Choosing the Right State Management Approach

Not all state needs a global store. Consider:

  • Component state: For UI-related state (modal visibility, form input).
  • Context API: For state shared across multiple components.
  • useReducer: For managing complex state logic.
  • External stores (Zustand, Redux): For global application state.

2. Using React Context for Global State

React Context is useful for global state, but should not be overused for frequently updated state.

Basic Context Example

import React, { createContext, useContext, useState } from "react";
const ThemeContext = createContext({ theme: "light", toggleTheme: () => {} });
export const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
const [theme, setTheme] = useState("light");
const toggleTheme = () => setTheme((prev) => (prev === "light" ? "dark" : "light"));
return <ThemeContext.Provider value={{ theme, toggleTheme }}>{children}</ThemeContext.Provider>;
};
export const useTheme = () => useContext(ThemeContext);

3. Managing Complex State with useReducer

For state transitions based on actions, useReducer is more scalable than useState.

Example: Counter with useReducer

import React, { useReducer } from "react";
type Action = { type: "increment" } | { type: "decrement" };
function reducer(state: number, action: Action): number {
switch (action.type) {
case "increment": return state + 1;
case "decrement": return state - 1;
default: return state;
}
}
const Counter = () => {
const [count, dispatch] = useReducer(reducer, 0);
return (
<div>
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
<span>{count}</span>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
</div>
);
};

4. Using Zustand for Lightweight Global State

Zustand is a simpler alternative to Redux, without unnecessary boilerplate.

Example: Global Store with Zustand

import create from "zustand";
interface Store {
count: number;
increase: () => void;
}
const useStore = create<Store>((set) => ({
count: 0,
increase: () => set((state) => ({ count: state.count + 1 })),
}));
const Counter = () => {
const { count, increase } = useStore();
return <button onClick={increase}>Count: {count}</button>;
};

5. Optimizing Performance with Selectors

Avoid unnecessary re-renders by using selectors.

const count = useStore((state) => state.count);

Only components that use count will re-render when it changes, improving efficiency.


6. Handling Asynchronous State

For fetching external data, use React Query or useEffect with state.

Fetching Data with React Query

import { useQuery } from "@tanstack/react-query";
const fetchUsers = async () => {
const res = await fetch("/api/users");
return res.json();
};
const UsersList = () => {
const { data, isLoading } = useQuery(["users"], fetchUsers);
if (isLoading) return <p>Loading...</p>;
return <ul>{data.map((user) => <li key={user.id}>{user.name}</li>)}</ul>;
};

Conclusion

  • Use local state for UI-related data.
  • Apply React Context selectively to avoid unnecessary re-renders.
  • Use useReducer for complex state logic.
  • Implement Zustand for a lightweight global state solution.
  • Optimize performance using selectors and memoization.

Efficient state management leads to better performance and maintainability in React applications.