Skip to content
intermediate Phase 9 · Frontend Architecture

State Management

Compare local state, context, Redux, and other state management solutions.

1h
0 problems
Topic Progress 0%

Local vs Global State

Local vs Global State

Understanding when to use local versus global state is crucial for maintainable applications.

Local State

State that belongs to a single component and doesn't need to be shared:

function Counter() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>Increment</button>
    </div>
  );
}

Lifted State

State moved to a parent when siblings need it:

function TemperatureConverter() {
  const [celsius, setCelsius] = useState(0);
  const fahrenheit = (celsius * 9/5) + 32;
  
  return (
    <div>
      <TemperatureInput 
        scale="celsius" 
        value={celsius}
        onChange={setCelsius}
      />
      <TemperatureInput
        scale="fahrenheit"
        value={fahrenheit}
        onChange={(f) => setCelsius((f - 32) * 5/9)}
      />
    </div>
  );
}

Global State

State shared across many components at different levels:

// Theme, User Authentication, Shopping Cart, UI State
const AppContext = createContext();

function AppProvider({ children }) {
  const [theme, setTheme] = useState('light');
  const [user, setUser] = useState(null);
  
  return (
    <AppContext.Provider value={{ theme, setTheme, user, setUser }}>
      {children}
    </AppContext.Provider>
  );
}

When to Use Each

  • Local State: Form inputs, toggle states, hover effects
  • Lifted State: Sibling coordination, simple shared state
  • Global State: Auth, theme, shopping cart, notifications

Context vs Redux

Context vs Redux

React Context

Built-in solution for sharing state across components:

const AuthContext = createContext(null);

function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    checkAuth().then(setUser).finally(() => setLoading(false));
  }, []);
  
  const login = async (credentials) => {
    const user = await loginUser(credentials);
    setUser(user);
  };
  
  const logout = async () => {
    await logoutUser();
    setUser(null);
  };
  
  if (loading) return <Spinner />;
  
  return (
    <AuthContext.Provider value={{ user, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

// Custom hook for consuming
function useAuth() {
  const context = useContext(AuthContext);
  if (!context) throw new Error('useAuth must be used within AuthProvider');
  return context;
}

Context Pros:

  • Built into React
  • No additional dependencies
  • Simple API

Context Cons:

  • Causes re-renders for all consumers when value changes
  • No middleware support
  • No dev tools integration

Redux

Predictable state container with powerful tooling:

// Slice
const authSlice = createSlice({
  name: 'auth',
  initialState: { user: null, loading: false, error: null },
  reducers: {
    loginStart: (state) => {
      state.loading = true;
      state.error = null;
    },
    loginSuccess: (state, action) => {
      state.loading = false;
      state.user = action.payload;
    },
    loginFailure: (state, action) => {
      state.loading = false;
      state.error = action.payload;
    },
    logout: (state) => {
      state.user = null;
    },
  },
});

// Async thunk
const login = createAsyncThunk(
  'auth/login',
  async (credentials, { rejectWithValue }) => {
    try {
      return await loginUser(credentials);
    } catch (error) {
      return rejectWithValue(error.message);
    }
  }
);

// Component
function LoginForm() {
  const dispatch = useDispatch();
  const { loading, error } = useSelector(state => state.auth);
  
  const handleSubmit = (e) => {
    e.preventDefault();
    dispatch(login({ email, password }));
  };
  
  return (
    <form onSubmit={handleSubmit}>
      {error && <div className="error">{error}</div>}
      <button disabled={loading}>Login</button>
    </form>
  );
}

Redux Pros:

  • Predictable state updates
  • Excellent dev tools
  • Middleware support
  • Selective re-renders

Redux Cons:

  • More boilerplate
  • Learning curve
  • Additional bundle size

State Management Libraries

State Management Libraries

Zustand

Minimal, fast state management:

import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';

const useStore = create(
  devtools(
    persist(
      (set, get) => ({
        // State
        bears: 0,
        users: [],
        
        // Actions
        increasePopulation: () => set((state) => ({ bears: state.bears + 1 }))
        removeAllBears: () => set({ bears: 0 })
        updateBears: (newBears) => set({ bears: newBears })
        fetchUsers: async () => {
          const users = await fetch('/api/users').then(r => r.json());
          set({ users });
        },
      }),
      { name: 'bear-storage' }
    )
  )
);

// Usage
function BearCounter() {
  const bears = useStore((state) => state.bears);
  const increase = useStore((state) => state.increasePopulation);
  
  return (
    <div>
      <span>{bears}</span>
      <button onClick={increase}>Add Bear</button>
    </div>
  );
}

Jotai

Atomic state management:

import { atom, useAtom } from 'jotai';

const countAtom = atom(0);
const doubleCountAtom = atom((get) => get(countAtom) * 2);
const incrementAtom = atom(null, (get, set) => {
  set(countAtom, get(countAtom) + 1);
});

function Counter() {
  const [count] = useAtom(countAtom);
  const [doubleCount] = useAtom(doubleCountAtom);
  const [, increment] = useAtom(incrementAtom);
  
  return (
    <div>
      <p>Count: {count}</p>
      <p>Double: {doubleCount}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

Recoil

Atom-based state management:

import { atom, useRecoilState, selector } from 'recoil';

const todoListState = atom({
  key: 'todoListState',
  default: [],
});

const todoListStatsState = selector({
  key: 'todoListStatsState',
  get: ({ get }) => {
    const todoList = get(todoListState);
    const totalNum = todoList.length;
    const totalCompletedNum = todoList.filter(item => item.isComplete).length;
    const totalUncompletedNum = totalNum - totalCompletedNum;
    const percentCompleted = totalNum === 0 ? 0 : totalCompletedNum / totalNum;
    
    return { totalNum, totalCompletedNum, totalUncompletedNum, percentCompleted };
  },
});

function Stats() {
  const { totalNum, percentCompleted } = useRecoilValue(todoListStatsState);
  return (
    <div>
      Total: {totalNum} | Completed: {Math.round(percentCompleted * 100)}%
    </div>
  );
}

Choosing the Right Tool

  • Local state only: useState/useReducer
  • Simple global state: Zustand
  • Atomic updates: Jotai or Recoil
  • Complex app with middleware: Redux Toolkit
  • Server state: React Query or SWR

Quiz

1. When should you use local state instead of global state?

Question 1 options

2. What is a major drawback of using React Context for complex state management?

Question 2 options

3. Which library provides excellent dev tools for state inspection?

Question 3 options

4. What is Zustand's main advantage?

Question 4 options

5. When should you use React Query or SWR instead of Redux?

Question 5 options

Flashcards

Question

What is the main difference between local and global state?

Answer

Local state is owned by a single component; global state is shared across many components.

Question

When should you choose Context over Redux?

Answer

For simple, infrequently changing global values like theme or auth status where re-renders aren't a concern.

Question

What is Zustand?

Answer

A minimal, fast state management library with a simple API and no boilerplate.

Question

What is the atomic state model?

Answer

Breaking state into small, independent atoms that can be composed and updated individually.

Revision Notes

Key Takeaways

  • 1. Use local state when data is only needed in one component
  • 2. Context is good for simple global state but causes re-renders
  • 3. Redux provides predictable updates, middleware, and dev tools
  • 4. Zustand offers minimal boilerplate for most state needs
  • 5. Server state should be managed with React Query or SWR

Interview Tips

  • Explain the trade-offs between Context and Redux
  • Discuss when you would choose Zustand over Redux
  • Know the performance implications of Context re-renders

Cheat Sheet

State Management Cheat Sheet

State Types

  • Local: useState, useReducer
  • Lifted: Props to parent
  • Global: Context, Redux, Zustand, Jotai
  • Server: React Query, SWR

Context Best Practices

  • Split into multiple providers by concern
  • Use memoization to prevent re-renders
  • Don't put rapidly changing data in context

Redux Toolkit

  • Use createSlice for reducers
  • Use createAsyncThunk for async
  • Use RTK Query for data fetching

Quick Decision

Need Solution
Simple shared state Context
Complex app state Redux Toolkit
Minimal overhead Zustand
Atomic updates Jotai/Recoil