Learn React in 10 DaysDay 8: Context and State Management
books.chapter 8Learn React in 10 Days

Day 8: Context and State Management

What You'll Learn Today

  • The Props drilling problem
  • Context API basics
  • The useContext hook
  • Context design patterns
  • Combining with useReducer

The Props Drilling Problem

When passing data to deeply nested components, you need to pass through intermediate components. This is called Props drilling.

flowchart TB
    subgraph Drilling["Props Drilling"]
        App["App<br/>user={user}"]
        Layout["Layout<br/>user={user}"]
        Main["Main<br/>user={user}"]
        Sidebar["Sidebar<br/>user={user}"]
        UserProfile["UserProfile<br/>uses user"]
    end

    App --> Layout --> Main --> Sidebar --> UserProfile

    style App fill:#ef4444,color:#fff
    style Layout fill:#f59e0b,color:#fff
    style Main fill:#f59e0b,color:#fff
    style Sidebar fill:#f59e0b,color:#fff
    style UserProfile fill:#22c55e,color:#fff

The Problem

// ❌ Props drilling: Intermediate components pass user without using it
function App() {
  const [user, setUser] = useState({ name: 'Alice', role: 'admin' });

  return <Layout user={user} />;
}

function Layout({ user }) {
  return (
    <div>
      <Header />
      <Main user={user} />  {/* Just passing */}
      <Footer />
    </div>
  );
}

function Main({ user }) {
  return <Sidebar user={user} />;  {/* Just passing */}
}

function Sidebar({ user }) {
  return <UserProfile user={user} />;  {/* Just passing */}
}

function UserProfile({ user }) {
  return <p>Welcome, {user.name}</p>;  {/* Actually uses it */}
}
TypeScript version
interface User {
  name: string;
  role: string;
}

interface UserProps {
  user: User;
}

// ❌ Props drilling: Intermediate components pass user without using it
function App(): React.JSX.Element {
  const [user, setUser] = useState<User>({ name: 'Alice', role: 'admin' });

  return <Layout user={user} />;
}

function Layout({ user }: UserProps): React.JSX.Element {
  return (
    <div>
      <Header />
      <Main user={user} />  {/* Just passing */}
      <Footer />
    </div>
  );
}

function Main({ user }: UserProps): React.JSX.Element {
  return <Sidebar user={user} />;  {/* Just passing */}
}

function Sidebar({ user }: UserProps): React.JSX.Element {
  return <UserProfile user={user} />;  {/* Just passing */}
}

function UserProfile({ user }: UserProps): React.JSX.Element {
  return <p>Welcome, {user.name}</p>;  {/* Actually uses it */}
}

What Is the Context API?

The Context API is a mechanism for "tunneling" data through the component tree.

flowchart TB
    subgraph Context["Context API"]
        Provider["Provider<br/>value={user}"]
        App["App"]
        Layout["Layout"]
        Main["Main"]
        Consumer["UserProfile<br/>useContext(UserContext)"]
    end

    Provider -.->|"Context"| Consumer
    Provider --> App --> Layout --> Main --> Consumer

    style Provider fill:#3b82f6,color:#fff
    style Consumer fill:#22c55e,color:#fff

Basic Context Usage

Step 1: Create the Context

import { createContext } from 'react';

// Create Context with default value
const UserContext = createContext(null);

export default UserContext;

Step 2: Wrap with Provider

import { useState } from 'react';
import UserContext from './UserContext';

function App() {
  const [user, setUser] = useState({ name: 'Alice', role: 'admin' });

  return (
    <UserContext.Provider value={user}>
      <Layout />
    </UserContext.Provider>
  );
}

Step 3: Use with useContext

import { useContext } from 'react';
import UserContext from './UserContext';

function UserProfile() {
  const user = useContext(UserContext);

  return <p>Welcome, {user.name}</p>;
}

Complete Code

import { createContext, useContext, useState } from 'react';

// Create Context
const UserContext = createContext(null);

// Top-level component
function App() {
  const [user, setUser] = useState({ name: 'Alice', role: 'admin' });

  return (
    <UserContext.Provider value={user}>
      <Layout />
    </UserContext.Provider>
  );
}

// Intermediate components (don't need to know about user)
function Layout() {
  return (
    <div>
      <Header />
      <Main />
      <Footer />
    </div>
  );
}

function Main() {
  return <Sidebar />;
}

function Sidebar() {
  return <UserProfile />;
}

// Component that uses Context
function UserProfile() {
  const user = useContext(UserContext);
  return <p>Welcome, {user.name}</p>;
}
TypeScript version
import { createContext, useContext, useState } from 'react';

interface User {
  name: string;
  role: string;
}

// Create Context
const UserContext = createContext<User | null>(null);

// Top-level component
function App(): React.JSX.Element {
  const [user, setUser] = useState<User>({ name: 'Alice', role: 'admin' });

  return (
    <UserContext.Provider value={user}>
      <Layout />
    </UserContext.Provider>
  );
}

// Intermediate components (don't need to know about user)
function Layout(): React.JSX.Element {
  return (
    <div>
      <Header />
      <Main />
      <Footer />
    </div>
  );
}

function Main(): React.JSX.Element {
  return <Sidebar />;
}

function Sidebar(): React.JSX.Element {
  return <UserProfile />;
}

// Component that uses Context
function UserProfile(): React.JSX.Element {
  const user = useContext(UserContext);
  return <p>Welcome, {user?.name}</p>;
}

Updatable Context

Provide state and update functions through Context.

import { createContext, useContext, useState } from 'react';

// Create Context
const ThemeContext = createContext(null);

// Provider component
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  function toggleTheme() {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  }

  const value = {
    theme,
    toggleTheme
  };

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
}

// Custom hook
function useTheme() {
  const context = useContext(ThemeContext);
  if (context === null) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return context;
}

// Usage
function App() {
  return (
    <ThemeProvider>
      <Header />
      <Main />
    </ThemeProvider>
  );
}

function Header() {
  const { theme, toggleTheme } = useTheme();

  return (
    <header style={{ background: theme === 'light' ? '#fff' : '#333' }}>
      <button onClick={toggleTheme}>
        {theme === 'light' ? 'πŸŒ™' : 'β˜€οΈ'}
      </button>
    </header>
  );
}
TypeScript version
import { createContext, useContext, useState, ReactNode } from 'react';

interface ThemeContextType {
  theme: 'light' | 'dark';
  toggleTheme: () => void;
}

// Create Context
const ThemeContext = createContext<ThemeContextType | null>(null);

// Provider component
interface ThemeProviderProps {
  children: ReactNode;
}

function ThemeProvider({ children }: ThemeProviderProps): React.JSX.Element {
  const [theme, setTheme] = useState<'light' | 'dark'>('light');

  function toggleTheme(): void {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  }

  const value: ThemeContextType = {
    theme,
    toggleTheme
  };

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
}

// Custom hook
function useTheme(): ThemeContextType {
  const context = useContext(ThemeContext);
  if (context === null) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return context;
}

// Usage
function App(): React.JSX.Element {
  return (
    <ThemeProvider>
      <Header />
      <Main />
    </ThemeProvider>
  );
}

function Header(): React.JSX.Element {
  const { theme, toggleTheme } = useTheme();

  return (
    <header style={{ background: theme === 'light' ? '#fff' : '#333' }}>
      <button onClick={toggleTheme}>
        {theme === 'light' ? 'πŸŒ™' : 'β˜€οΈ'}
      </button>
    </header>
  );
}

Combining Multiple Contexts

// Auth Context
const AuthContext = createContext(null);

function AuthProvider({ children }) {
  const [user, setUser] = useState(null);

  const login = (userData) => setUser(userData);
  const logout = () => setUser(null);

  return (
    <AuthContext.Provider value={{ user, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

// Theme Context
const ThemeContext = createContext(null);

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  const toggleTheme = () => setTheme(t => t === 'light' ? 'dark' : 'light');

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

// Combining multiple Providers
function App() {
  return (
    <AuthProvider>
      <ThemeProvider>
        <MainApp />
      </ThemeProvider>
    </AuthProvider>
  );
}
TypeScript version
import { createContext, useState, ReactNode } from 'react';

// Auth Context
interface User {
  name: string;
  role: string;
}

interface AuthContextType {
  user: User | null;
  login: (userData: User) => void;
  logout: () => void;
}

const AuthContext = createContext<AuthContextType | null>(null);

function AuthProvider({ children }: { children: ReactNode }): React.JSX.Element {
  const [user, setUser] = useState<User | null>(null);

  const login = (userData: User): void => setUser(userData);
  const logout = (): void => setUser(null);

  return (
    <AuthContext.Provider value={{ user, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

// Theme Context
interface ThemeContextType {
  theme: 'light' | 'dark';
  toggleTheme: () => void;
}

const ThemeContext = createContext<ThemeContextType | null>(null);

function ThemeProvider({ children }: { children: ReactNode }): React.JSX.Element {
  const [theme, setTheme] = useState<'light' | 'dark'>('light');
  const toggleTheme = (): void => setTheme(t => t === 'light' ? 'dark' : 'light');

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

// Combining multiple Providers
function App(): React.JSX.Element {
  return (
    <AuthProvider>
      <ThemeProvider>
        <MainApp />
      </ThemeProvider>
    </AuthProvider>
  );
}
flowchart TB
    subgraph Providers["Multiple Providers"]
        Auth["AuthProvider"]
        Theme["ThemeProvider"]
        App["App Components"]
    end

    Auth --> Theme --> App

    style Auth fill:#3b82f6,color:#fff
    style Theme fill:#8b5cf6,color:#fff

Combining with useReducer

Use useReducer for complex state management.

useReducer Basics

import { useReducer } from 'react';

// Initial state
const initialState = { count: 0 };

// Reducer function
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return initialState;
    default:
      throw new Error(`Unknown action: ${action.type}`);
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
    </div>
  );
}
TypeScript version
import { useReducer } from 'react';

interface CounterState {
  count: number;
}

type CounterAction =
  | { type: 'increment' }
  | { type: 'decrement' }
  | { type: 'reset' };

// Initial state
const initialState: CounterState = { count: 0 };

// Reducer function
function reducer(state: CounterState, action: CounterAction): CounterState {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return initialState;
    default:
      throw new Error(`Unknown action: ${(action as { type: string }).type}`);
  }
}

function Counter(): React.JSX.Element {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
    </div>
  );
}

Context + useReducer Combined

import { createContext, useContext, useReducer, useState } from 'react';

// Todo type (in comments)
// { id: number, text: string, completed: boolean }

// Initial state
const initialState = {
  todos: [],
  filter: 'all'  // 'all' | 'active' | 'completed'
};

// Reducer
function todoReducer(state, action) {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [
          ...state.todos,
          { id: Date.now(), text: action.payload, completed: false }
        ]
      };

    case 'TOGGLE_TODO':
      return {
        ...state,
        todos: state.todos.map(todo =>
          todo.id === action.payload
            ? { ...todo, completed: !todo.completed }
            : todo
        )
      };

    case 'DELETE_TODO':
      return {
        ...state,
        todos: state.todos.filter(todo => todo.id !== action.payload)
      };

    case 'SET_FILTER':
      return {
        ...state,
        filter: action.payload
      };

    default:
      return state;
  }
}

// Context
const TodoContext = createContext(null);

// Provider
function TodoProvider({ children }) {
  const [state, dispatch] = useReducer(todoReducer, initialState);

  // Filtered todos
  const filteredTodos = state.todos.filter(todo => {
    if (state.filter === 'active') return !todo.completed;
    if (state.filter === 'completed') return todo.completed;
    return true;
  });

  const value = {
    todos: filteredTodos,
    allTodos: state.todos,
    filter: state.filter,
    dispatch
  };

  return (
    <TodoContext.Provider value={value}>
      {children}
    </TodoContext.Provider>
  );
}

// Custom hook
function useTodo() {
  const context = useContext(TodoContext);
  if (!context) {
    throw new Error('useTodo must be used within TodoProvider');
  }
  return context;
}

// Components
function TodoApp() {
  return (
    <TodoProvider>
      <h1>Todo App</h1>
      <AddTodo />
      <FilterButtons />
      <TodoList />
      <TodoStats />
    </TodoProvider>
  );
}

function AddTodo() {
  const { dispatch } = useTodo();
  const [text, setText] = useState('');

  function handleSubmit(e) {
    e.preventDefault();
    if (text.trim()) {
      dispatch({ type: 'ADD_TODO', payload: text });
      setText('');
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="New task"
      />
      <button type="submit">Add</button>
    </form>
  );
}

function FilterButtons() {
  const { filter, dispatch } = useTodo();

  return (
    <div>
      {['all', 'active', 'completed'].map(f => (
        <button
          key={f}
          onClick={() => dispatch({ type: 'SET_FILTER', payload: f })}
          style={{ fontWeight: filter === f ? 'bold' : 'normal' }}
        >
          {f}
        </button>
      ))}
    </div>
  );
}

function TodoList() {
  const { todos, dispatch } = useTodo();

  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          <input
            type="checkbox"
            checked={todo.completed}
            onChange={() => dispatch({ type: 'TOGGLE_TODO', payload: todo.id })}
          />
          <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
            {todo.text}
          </span>
          <button onClick={() => dispatch({ type: 'DELETE_TODO', payload: todo.id })}>
            Delete
          </button>
        </li>
      ))}
    </ul>
  );
}

function TodoStats() {
  const { allTodos } = useTodo();
  const completed = allTodos.filter(t => t.completed).length;

  return (
    <p>
      Completed: {completed} / {allTodos.length}
    </p>
  );
}
TypeScript version
import { createContext, useContext, useReducer, useState, ReactNode, Dispatch } from 'react';

// Types
interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

type Filter = 'all' | 'active' | 'completed';

interface TodoState {
  todos: Todo[];
  filter: Filter;
}

type TodoAction =
  | { type: 'ADD_TODO'; payload: string }
  | { type: 'TOGGLE_TODO'; payload: number }
  | { type: 'DELETE_TODO'; payload: number }
  | { type: 'SET_FILTER'; payload: Filter };

// Initial state
const initialState: TodoState = {
  todos: [],
  filter: 'all'
};

// Reducer
function todoReducer(state: TodoState, action: TodoAction): TodoState {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [
          ...state.todos,
          { id: Date.now(), text: action.payload, completed: false }
        ]
      };

    case 'TOGGLE_TODO':
      return {
        ...state,
        todos: state.todos.map(todo =>
          todo.id === action.payload
            ? { ...todo, completed: !todo.completed }
            : todo
        )
      };

    case 'DELETE_TODO':
      return {
        ...state,
        todos: state.todos.filter(todo => todo.id !== action.payload)
      };

    case 'SET_FILTER':
      return {
        ...state,
        filter: action.payload
      };

    default:
      return state;
  }
}

// Context
interface TodoContextType {
  todos: Todo[];
  allTodos: Todo[];
  filter: Filter;
  dispatch: Dispatch<TodoAction>;
}

const TodoContext = createContext<TodoContextType | null>(null);

// Provider
function TodoProvider({ children }: { children: ReactNode }): React.JSX.Element {
  const [state, dispatch] = useReducer(todoReducer, initialState);

  const filteredTodos = state.todos.filter(todo => {
    if (state.filter === 'active') return !todo.completed;
    if (state.filter === 'completed') return todo.completed;
    return true;
  });

  const value: TodoContextType = {
    todos: filteredTodos,
    allTodos: state.todos,
    filter: state.filter,
    dispatch
  };

  return (
    <TodoContext.Provider value={value}>
      {children}
    </TodoContext.Provider>
  );
}

// Custom hook
function useTodo(): TodoContextType {
  const context = useContext(TodoContext);
  if (!context) {
    throw new Error('useTodo must be used within TodoProvider');
  }
  return context;
}

// Components
function TodoApp(): React.JSX.Element {
  return (
    <TodoProvider>
      <h1>Todo App</h1>
      <AddTodo />
      <FilterButtons />
      <TodoList />
      <TodoStats />
    </TodoProvider>
  );
}

function AddTodo(): React.JSX.Element {
  const { dispatch } = useTodo();
  const [text, setText] = useState<string>('');

  function handleSubmit(e: React.FormEvent<HTMLFormElement>): void {
    e.preventDefault();
    if (text.trim()) {
      dispatch({ type: 'ADD_TODO', payload: text });
      setText('');
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={text}
        onChange={(e: React.ChangeEvent<HTMLInputElement>) => setText(e.target.value)}
        placeholder="New task"
      />
      <button type="submit">Add</button>
    </form>
  );
}

function FilterButtons(): React.JSX.Element {
  const { filter, dispatch } = useTodo();
  const filters: Filter[] = ['all', 'active', 'completed'];

  return (
    <div>
      {filters.map(f => (
        <button
          key={f}
          onClick={() => dispatch({ type: 'SET_FILTER', payload: f })}
          style={{ fontWeight: filter === f ? 'bold' : 'normal' }}
        >
          {f}
        </button>
      ))}
    </div>
  );
}

function TodoList(): React.JSX.Element {
  const { todos, dispatch } = useTodo();

  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          <input
            type="checkbox"
            checked={todo.completed}
            onChange={() => dispatch({ type: 'TOGGLE_TODO', payload: todo.id })}
          />
          <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
            {todo.text}
          </span>
          <button onClick={() => dispatch({ type: 'DELETE_TODO', payload: todo.id })}>
            Delete
          </button>
        </li>
      ))}
    </ul>
  );
}

function TodoStats(): React.JSX.Element {
  const { allTodos } = useTodo();
  const completed = allTodos.filter(t => t.completed).length;

  return (
    <p>
      Completed: {completed} / {allTodos.length}
    </p>
  );
}

Context Best Practices

Proper Separation

// ❌ Too much in one Context
const AppContext = createContext({
  user: null,
  theme: 'light',
  language: 'en',
  notifications: [],
  cart: [],
  // ...
});

// βœ… Separate by concern
const AuthContext = createContext(null);
const ThemeContext = createContext(null);
const LanguageContext = createContext(null);
const NotificationContext = createContext(null);
const CartContext = createContext(null);

Performance Considerations

// ❌ Creating new object each render (unnecessary re-renders)
function BadProvider({ children }) {
  const [count, setCount] = useState(0);

  return (
    <MyContext.Provider value={{ count, setCount }}>
      {children}
    </MyContext.Provider>
  );
}

// βœ… Memoize with useMemo
function GoodProvider({ children }) {
  const [count, setCount] = useState(0);

  const value = useMemo(() => ({ count, setCount }), [count]);

  return (
    <MyContext.Provider value={value}>
      {children}
    </MyContext.Provider>
  );
}

Choosing the Right Approach

Scenario Recommended Approach
2-3 levels of Props Props are sufficient
Theme, auth, language Context
Global state management Context + useReducer
Complex app-wide state Consider external libraries
flowchart TB
    A["State Management Choice"] --> B{"Deep hierarchy?"}
    B -->|No| C["Pass with Props"]
    B -->|Yes| D{"Complex state?"}
    D -->|No| E["Context + useState"]
    D -->|Yes| F["Context + useReducer"]

    style C fill:#22c55e,color:#fff
    style E fill:#3b82f6,color:#fff
    style F fill:#8b5cf6,color:#fff

Summary

Concept Description
Props drilling Problem of passing Props through deep hierarchies
Context Share data across the component tree
Provider Component that provides Context values
useContext Hook to retrieve Context values
useReducer Manage complex state update logic

Key Takeaways

  1. Context is ideal for global state
  2. Encapsulate Context usage in custom hooks
  3. Separate Contexts by concern
  4. Combine useReducer for complex state
  5. Use useMemo for performance

Exercises

Exercise 1: Basics

Create a Context for language settings (English/Japanese). Add a button to switch languages and display text that changes accordingly.

Exercise 2: Application

Create a shopping cart Context:

  • Add/remove products
  • Change quantities
  • Calculate total price

Challenge

Create an authentication system Context (using useReducer):

  • Login/logout functionality
  • Loading state management
  • Error message handling
  • Routing based on auth state

References


Coming Up Next: On Day 9, we'll learn about "Performance Optimization." Understand techniques to keep your React apps fast.