Skip to content
intermediatePhase 37 · Frontend Architecture

State Management

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

1h
0 problems
Topic Progress0%

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

Practice Problems

0/3solved
Build State Management Component

Create a reusable React component implementing State Management. Include proper state management and accessibility.

Solution
// Production-ready component with:
// - Proper TypeScript types
// - Accessibility (ARIA)
// - Error boundaries
// - Loading states
// - Memoization where needed
State Management Testing

Write unit and integration tests for State Management using React Testing Library.

Solution
// Test coverage:
// 1. Rendering tests
// 2. Interaction tests
// 3. Edge case tests
// 4. Accessibility tests
State Management Performance

Optimize State Management for performance. Consider memoization, code splitting, and bundle size.

Solution
// Optimization techniques:
// 1. React.memo / useMemo / useCallback
// 2. Code splitting with lazy()
// 3. Virtual scrolling for lists
// 4. Image lazy loading
// 5. Bundle analysis

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.

Question

What is State Management?

Answer

State Management is a key concept in frontend development.

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