startupbricks logo

Startupbricks

State Management in React: Complete Guide

State Management in React: Complete Guide

2025-01-16
7 min read
Frontend Development

Here's what confuses every React developer:

"When do I need Redux? Zustand? Context? What about hooks?"

Every article says something different. Every example uses different approach.

Let me clear this up once and for all.

This guide shows you exactly when to use each state management option—with code examples.


State Management Spectrum: Simple to Complex

Not all state is same. Different problems need different solutions.

Level 1: Local Component State (Simplest)

Use For:

  • Form inputs (name, email, password)
  • UI state (open/close modals, tabs, dropdowns)
  • Component-level toggles (dark mode, sidebar)
  • Temporary data (loading states, error messages)

Examples:

javascript
const [name, setName] = useState("");
const [isOpen, setIsOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);

Don't Use External Libraries For:

  • Simple form fields
  • Component toggles
  • UI-only state

Level 2: React Context (Mid Complexity)

Use For:

  • Theme (dark mode, language)
  • User authentication status
  • Settings that multiple components need
  • Data accessed by many components (not updated frequently)

When Context Works Well:

  • State doesn't change often
  • Children read more than write
  • Simple data structures (user, theme, settings)
  • Performance isn't critical concern

Example:

javascript
// Create context
const AuthContext = createContext();
// Provider
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const login = async (email, password) => {
const user = await api.login(email, password);
setUser(user);
};
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}
// Consume
export function useAuth() {
return useContext(AuthContext);
}

Level 3: State Management Library (Complex)

Use For:

  • Complex state with many actions
  • State accessed by many components
  • Frequent updates across components
  • Performance matters for large state
  • Need time-travel debugging (undo/redo)

Popular Libraries (2025):

  • Zustand: Modern, simple, fast
  • Jotai: Atomic, flexible, powerful
  • Redux Toolkit: Mature, ecosystem, Redux DevTools
  • Recoil: Facebook's solution, experimental but good

When to Use Each Approach

Decision Framework

Situation

Use This Approach

Form inputs, UI toggles

Local useState

Theme, auth, simple settings

Context API

Multiple components write frequently

Zustand/Jotai

Complex state with many actions

Redux Toolkit

Need undo/redo/time-travel

Redux Toolkit

Performance critical with many updates

Zustand or Jotai

Example Scenarios

Scenario 1: Shopping Cart (Complex)

javascript
// Good: Zustand
const useCartStore = create((set) => ({
items: [],
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
removeItem: (id) =>
set((state) => ({ items: state.items.filter((i) => i.id !== id) })),
clear: () => set({ items: [] }),
}));

Scenario 2: Theme (Simple)

javascript
// Good: Context
const ThemeContext = createContext();
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState("light");
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}

React Hooks: Use Before External Libraries

Built-in hooks are powerful. Use them first.

useState

For: Simple, independent state in component

javascript
const [count, setCount] = useState(0);

useReducer

For: Complex state logic with multiple related state values

javascript
const initialState = { count: 0, name: "" };
function reducer(state, action) {
switch (action.type) {
case "increment":
return { ...state, count: state.count + 1 };
case "setName":
return { ...state, name: action.payload };
default:
return state;
}
}
const [state, dispatch] = useReducer(reducer, initialState);

useEffect

For: Side effects (API calls, subscriptions, DOM manipulation)

javascript
useEffect(() => {
// Runs after render
fetchData();
}, []); // Empty deps = run once
useEffect(() => {
// Runs when userId changes
fetchUserData(userId);
}, [userId]);

useCallback

For: Memoizing functions to prevent unnecessary re-renders

javascript
const handleClick = useCallback(() => {
doSomething(id);
}, [id]); // Only recreates when id changes

useMemo

For: Expensive calculations

javascript
const expensiveValue = useMemo(() => {
return computeExpensive(data);
}, [data]); // Only re-computes when data changes

React Context: For Shared State

Context is built-in, works well for right use cases.

When to Use Context

Good Use Cases:

  • User authentication (login status)
  • Theme (dark/light mode)
  • Language (i18n)
  • App-wide settings
  • Data read often, written rarely

Bad Use Cases:

  • Frequently updated data (real-time feeds, chat)
  • Complex state with many actions
  • Performance-critical updates
  • State that changes on every render

Context Best Practices

1. Split Multiple Contexts

javascript
// Bad: Everything in one context
const AppContext = createContext({
user: null,
theme: "light",
cart: [],
notifications: [],
settings: {},
});
// Good: Separate concerns
const UserContext = createContext({ user, login, logout });
const ThemeContext = createContext({ theme, setTheme });
const CartContext = createContext({ cart, addItem, removeItem });

2. Memoize Context Providers

javascript
// Prevents re-renders when parent updates
export function AppProviders({ children }) {
return (
<AuthProvider>
<ThemeProvider>
<CartProvider>{children}</CartProvider>
</ThemeProvider>
</AuthProvider>
);
}

3. Optimize Context Values

javascript
// Bad: Re-creates object every render
<UserContext.Provider value={{ user, login, logout }}>
// Good: Memoize if expensive
<UserContext.Provider value={{ user, login, logout }}>

Zustand: Modern State Management (Recommended)

Zustand is our favorite for most use cases in 2025.

Why Zustand?

Pros:

  • No boilerplate (vs Redux)
  • No providers needed (Context-based under hood)
  • TypeScript support excellent
  • Fast (no context re-renders)
  • Simple API
  • Tiny bundle (1KB)

Cons:

  • Smaller ecosystem than Redux
  • No DevTools (yet, though coming)
  • Newer (less battle-tested than Redux)

Zustand Example

typescript
// store.ts
import create from "zustand";
interface User {
id: string;
name: string;
email: string;
}
interface AuthStore {
user: User | null;
isLoading: boolean;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
}
const useAuthStore = create<AuthStore>((set) => ({
user: null,
isLoading: false,
login: async (email, password) => {
set({ isLoading: true });
try {
const user = await api.login(email, password);
set({ user, isLoading: false });
} catch (error) {
set({ isLoading: false });
throw error;
}
},
logout: () => {
set({ user: null });
api.logout();
},
}));
// Component usage
function Login() {
const { login, isLoading } = useAuthStore();
const handleSubmit = async (e) => {
e.preventDefault();
await login(email, password);
};
return <form onSubmit={handleSubmit}>...</form>;
}

Jotai: Atomic State Management

Jotai is great for very complex, deeply nested state.

Why Jotai?

Pros:

  • Atomic state (small, independent pieces)
  • Flexible composition
  • TypeScript excellent
  • Very small bundle
  • No providers needed

Cons:

  • More verbose than Zustand for simple cases
  • Newer, smaller ecosystem

Jotai Example

typescript
// store.ts
import { atom, useAtom } from "jotai";
// Atomic state pieces
export const countAtom = atom(0);
export const nameAtom = atom("");
export const emailAtom = atom("");
// Component usage
function Counter() {
const [count, setCount] = useAtom(countAtom);
return (
<div>
<button onClick={() => setCount(count + 1)}>{count}</button>
</div>
);
}

Redux Toolkit: For Complex Apps

Redux is mature but has boilerplate. Redux Toolkit fixes this.

When to Use Redux Toolkit

  • Very complex state (dozens of actions, reducers)
  • Need time-travel debugging
  • Team already knows Redux
  • Need middleware (thunk, saga, etc.)
  • Want large ecosystem support

Redux Toolkit Example

typescript
// store/slice.ts
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
interface CounterState {
value: number;
}
const initialState: CounterState = {
value: 0,
};
export const counterSlice = createSlice({
name: "counter",
initialState,
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload;
},
},
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;

State Management Decision Tree

Use this flowchart to decide:

┌─────────────────────────────┐
│ Is state local to component? │
└──────────────┬──────────────┘
               │ Yes
               ↓
          Use useState/useReducer
               │ No
               ↓
┌──────────────────────────────┐
│ Is state complex or updates │
│ frequently across components?  │
└──────────────┬───────────────┘
               │ Yes
               ↓
┌─────────────────────────────┐
│ Do you need DevTools or   │
│ time-travel debugging?        │
└──────────────┬──────────────┘
               │ Yes        │ No
               ↓            ↓
        Redux Toolkit     Zustand/Jotai

Performance: When State Management Hurts

Common Performance Issues

1. Unnecessary Re-renders

javascript
// Bad: Re-renders every render
<Parent>
<Child onClick={handleClick} />
</Parent>
// Good: Memoize function
<Parent>
<Child onClick={useCallback(handleClick, [dependency])} />
</Parent>

2. Large State Objects

javascript
// Bad: Entire user object in Context
<UserContext.Provider value={{ user }}>
// Good: Only what's needed
<UserContext.Provider value={{ userId, userName }}>

3. Frequent Context Updates

javascript
// Bad: Context updates every second
<TimerContext.Provider value={{ currentTime }}> // All consumers re-render
// Good: Use ref or state library with selective subscriptions

State Management Best Practices

1. Keep State as Simple as Possible

Start with useState. Only escalate when you clearly need more.

2. Colocate Related State

Don't scatter related state across different stores.

Bad:

javascript
const useUserStore = ...;
const useUserProfileStore = ...;
const useUserSettingsStore = ...;
const useUserNotificationsStore = ...;

Good:

javascript
const useUserStore = ...; // Everything user-related together

3. Derive State When Possible

Don't duplicate data, compute it.

javascript
// Bad: Store both first and last name
const userStore = create((set) => ({
firstName: "",
lastName: "",
fullName: "", // Duplicated!
}));
// Good: Compute fullName
function Component() {
const { firstName, lastName } = useUserStore();
const fullName = `${firstName} ${lastName}`; // Derived, not stored
}

4. Use TypeScript

State is your most critical data. Type it.

typescript
interface User {
id: string;
name: string;
email: string;
}
interface UserStore {
user: User | null;
login: (email: string, password: string) => Promise<void>;
}

State Management Comparison 2025

Feature

useState

Context

Zustand

Jotai

Redux Toolkit

Bundle Size

0KB

~1KB

~1KB

~3KB

~5KB

TypeScript

Excellent

Good

Excellent

Excellent

Excellent

Performance

Fast

Medium (re-renders)

Fast

Fast

Fast

DevTools

Browser DevTools

Browser DevTools

Planning

Browser DevTools

Redux DevTools

Ecosystem

React

React

Growing

Growing

Largest

Learning Curve

Easy

Easy

Easy

Easy

Medium

Use Cases

Local

Simple shared

Most cases

Atomic

Complex

Bundle Size

0KB

~1KB

~1KB

~3KB

~5KB

TypeScript

Excellent

Good

Excellent

Excellent

Excellent

Performance

Fast

Medium (re-renders)

Fast

Fast

Fast

DevTools

Browser DevTools

Browser DevTools

Planning

Browser DevTools

Redux DevTools

Ecosystem

React

React

Growing

Growing

Largest

Learning Curve

Easy

Easy

Easy

Easy

Medium

Use Cases

Local

Simple shared

Most cases

Atomic

Complex


Related Reading

If you found this helpful, you might also enjoy:


Need Help Choosing State Management?

At Startupbricks, we've built dozens of React apps with different state management approaches. We know what works for different use cases, team sizes, and performance requirements.

Whether you need:

  • State management architecture design
  • Performance optimization
  • Code review and refactoring
  • Team training and best practices

Let's talk about choosing right state management for your app.

Ready to choose? Download our free State Management Decision Tree and start today.

Share: