dev.log / syntax diaries

Practical code notes, tools, and guided learning for developers.

Practical guides, developer tools, and tutorials for modern web developers, with the same focused tone across writing, utilities, and learning tracks.

BlogToolsTutorialsAboutContactAdmin Login
Privacy PolicyTerms of ServiceCookie Policy

ยฉ 2026 The Syntax Diaries ยท System_Operational

The Syntax Diaries logoThe Syntax Diaries
BlogToolsTutorialsAbout
build log live
Tutorial / React
Advanced Hooks40 minbeginner

useReducer Hook

Learn how to manage complex state logic with useReducer, when to use it over useState, and how to build scalable state management patterns.

On This Page

The Problem with Complex useStateWhat is useReducer?useReducer BasicsBuilding a Real Shopping Cart with useReducerWhen to Use useReducer vs useStateUse useState when:Use useReducer when:Real-World Example: Form ManagementCombining useReducer with useContextPractice Exercise: Build a Todo App with CategoriesCommon useReducer PatternsPattern 1: Action CreatorsPattern 2: Async Actions with useEffectPattern 3: Middleware-like LoggingWhat We've LearnedQuick Recap QuizWhat's Next?

useReducer Hook#

Imagine you're managing a complex machine with many buttons, switches, and dials. useState is like having separate controls for each part, but sometimes you need a central control panel that coordinates everything based on what action you want to perform.

That's exactly what useReducer does! When your component's state becomes complex with multiple related pieces of data that change together, useReducer provides a more organized and predictable way to manage updates.

The Problem with Complex useState#

Let's start by understanding when useState becomes unwieldy. Imagine you're building a shopping cart component:

function ShoppingCart() {
  const [items, setItems] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [total, setTotal] = useState(0);
  const [discountCode, setDiscountCode] = useState('');
  const [discountAmount, setDiscountAmount] = useState(0);
  
  const addItem = (product) => {
    setLoading(true);
    setError(null);
    
    // Simulate API call
    setTimeout(() => {
      setItems(prev => [...prev, product]);
      setTotal(prev => prev + product.price);
      setLoading(false);
    }, 1000);
  };
  
  const removeItem = (productId) => {
    setItems(prev => prev.filter(item => item.id !== productId));
    const removedItem = items.find(item => item.id === productId);
    if (removedItem) {
      setTotal(prev => prev - removedItem.price);
    }
  };
  
  const applyDiscount = (code) => {
    setLoading(true);
    
    // Simulate validation
    setTimeout(() => {
      if (code === 'SAVE10') {
        setDiscountCode(code);
        setDiscountAmount(total * 0.1);
      } else {
        setError('Invalid discount code');
      }
      setLoading(false);
    }, 500);
  };
  
  // This component is getting complex and error-prone!
}

Problems with this approach:

  • ๐Ÿ”„ Multiple state updates scattered throughout the component
  • ๐Ÿ› Easy to forget updating related state (like forgetting to update total)
  • ๐Ÿ”€ State can get out of sync (items and total might not match)
  • ๐Ÿ“ Hard to test individual state logic
  • ๐Ÿง  Difficult to reason about all possible state combinations

What is useReducer?#

useReducer is React's solution for managing complex state. Instead of multiple useState calls, you have:

  1. One state object that holds all related data
  2. A reducer function that describes how state changes
  3. Actions that describe what happened
  4. Predictable updates that are easy to test and debug

Think of it like this:

  • useState: "Set the temperature to 72 degrees"
  • useReducer: "Someone pressed the heating button" โ†’ reducer figures out what that means

useReducer Basics#

Here's the basic syntax:

import React, { useReducer } from 'react';

// 1. Define your initial state
const initialState = {
  count: 0,
  step: 1
};

// 2. Define your reducer function
function counterReducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { ...state, count: state.count + state.step };
    case 'decrement':
      return { ...state, count: state.count - state.step };
    case 'setStep':
      return { ...state, step: action.payload };
    case 'reset':
      return initialState;
    default:
      throw new Error('Unknown action type: ' + action.type);
  }
}

// 3. Use it in your component
function Counter() {
  const [state, dispatch] = useReducer(counterReducer, initialState);
  
  return (
    <div>
      <h2>Count: {state.count}</h2>
      <p>Step size: {state.step}</p>
      
      <button onClick={() => dispatch({ type: 'increment' })}>
        +{state.step}
      </button>
      <button onClick={() => dispatch({ type: 'decrement' })}>
        -{state.step}
      </button>
      
      <div>
        <label>
          Step size:
          <input
            type="number"
            value={state.step}
            onChange={(e) => dispatch({
              type: 'setStep',
              payload: Number(e.target.value)
            })}
          />
        </label>
      </div>
      
      <button onClick={() => dispatch({ type: 'reset' })}>
        Reset
      </button>
    </div>
  );
}

Key concepts:

  • State: One object containing all your component's data
  • Action: An object describing what happened (always has a type)
  • Dispatch: A function to send actions to the reducer
  • Reducer: A pure function that takes current state + action and returns new state

Building a Real Shopping Cart with useReducer#

Let's rebuild our shopping cart using useReducer to see the difference:

// 1. Define initial state
const initialCartState = {
  items: [],
  total: 0,
  loading: false,
  error: null,
  discountCode: '',
  discountAmount: 0
};

// 2. Define reducer function
function cartReducer(state, action) {
  switch (action.type) {
    case 'ADD_ITEM_START':
      return {
        ...state,
        loading: true,
        error: null
      };
      
    case 'ADD_ITEM_SUCCESS':
      const newItem = action.payload;
      return {
        ...state,
        items: [...state.items, newItem],
        total: state.total + newItem.price,
        loading: false
      };
      
    case 'ADD_ITEM_ERROR':
      return {
        ...state,
        loading: false,
        error: action.payload
      };
      
    case 'REMOVE_ITEM':
      const itemToRemove = state.items.find(item => item.id === action.payload);
      return {
        ...state,
        items: state.items.filter(item => item.id !== action.payload),
        total: state.total - (itemToRemove ? itemToRemove.price : 0)
      };
      
    case 'APPLY_DISCOUNT_START':
      return {
        ...state,
        loading: true,
        error: null
      };
      
    case 'APPLY_DISCOUNT_SUCCESS':
      return {
        ...state,
        discountCode: action.payload.code,
        discountAmount: action.payload.amount,
        loading: false
      };
      
    case 'APPLY_DISCOUNT_ERROR':
      return {
        ...state,
        error: action.payload,
        loading: false
      };
      
    case 'CLEAR_CART':
      return {
        ...initialCartState
      };
      
    default:
      throw new Error('Unknown action type: ' + action.type);
  }
}

// 3. Use in component
function ShoppingCart() {
  const [state, dispatch] = useReducer(cartReducer, initialCartState);
  
  const addItem = async (product) => {
    dispatch({ type: 'ADD_ITEM_START' });
    
    try {
      // Simulate API call
      await new Promise(resolve => setTimeout(resolve, 1000));
      
      dispatch({
        type: 'ADD_ITEM_SUCCESS',
        payload: { ...product, id: Date.now() }
      });
    } catch (error) {
      dispatch({
        type: 'ADD_ITEM_ERROR',
        payload: 'Failed to add item to cart'
      });
    }
  };
  
  const removeItem = (itemId) => {
    dispatch({ type: 'REMOVE_ITEM', payload: itemId });
  };
  
  const applyDiscount = async (code) => {
    dispatch({ type: 'APPLY_DISCOUNT_START' });
    
    try {
      // Simulate discount validation
      await new Promise(resolve => setTimeout(resolve, 500));
      
      if (code === 'SAVE10') {
        dispatch({
          type: 'APPLY_DISCOUNT_SUCCESS',
          payload: {
            code: code,
            amount: state.total * 0.1
          }
        });
      } else {
        throw new Error('Invalid discount code');
      }
    } catch (error) {
      dispatch({
        type: 'APPLY_DISCOUNT_ERROR',
        payload: error.message
      });
    }
  };
  
  const finalTotal = state.total - state.discountAmount;
  
  return (
    <div style={{ padding: '20px', maxWidth: '600px' }}>
      <h2>๐Ÿ›’ Shopping Cart</h2>
      
      {/* Error display */}
      {state.error && (
        <div style={{
          color: 'red',
          backgroundColor: '#ffebee',
          padding: '10px',
          borderRadius: '5px',
          marginBottom: '15px'
        }}>
          โŒ {state.error}
        </div>
      )}
      
      {/* Sample products */}
      <div style={{ marginBottom: '20px' }}>
        <h3>Available Products</h3>
        <button 
          onClick={() => addItem({ name: 'Cool T-Shirt', price: 19.99 })}
          disabled={state.loading}
        >
          Add T-Shirt ($19.99)
        </button>
        <button 
          onClick={() => addItem({ name: 'Nice Shoes', price: 89.99 })}
          disabled={state.loading}
        >
          Add Shoes ($89.99)
        </button>
        <button 
          onClick={() => addItem({ name: 'Awesome Hat', price: 24.99 })}
          disabled={state.loading}
        >
          Add Hat ($24.99)
        </button>
      </div>
      
      {/* Cart items */}
      <div style={{ marginBottom: '20px' }}>
        <h3>Cart Items ({state.items.length})</h3>
        {state.items.length === 0 ? (
          <p>Your cart is empty</p>
        ) : (
          <ul style={{ listStyle: 'none', padding: 0 }}>
            {state.items.map(item => (
              <li key={item.id} style={{
                display: 'flex',
                justifyContent: 'space-between',
                alignItems: 'center',
                padding: '10px',
                border: '1px solid #ddd',
                marginBottom: '5px',
                borderRadius: '5px'
              }}>
                <span>{item.name} - $" + item.price + "</span>
                <button onClick={() => removeItem(item.id)}>
                  Remove
                </button>
              </li>
            ))}
          </ul>
        )}
      </div>
      
      {/* Discount section */}
      {state.items.length > 0 && (
        <div style={{ marginBottom: '20px' }}>
          <h4>Apply Discount</h4>
          <input
            type="text"
            placeholder="Enter discount code (try SAVE10)"
            value={state.discountCode}
            onChange={(e) => {/* We could add SET_DISCOUNT_CODE action */}}
            style={{ marginRight: '10px', padding: '5px' }}
          />
          <button 
            onClick={() => applyDiscount('SAVE10')}
            disabled={state.loading}
          >
            Apply Discount
          </button>
          
          {state.discountAmount > 0 && (
            <div style={{ color: 'green', marginTop: '5px' }}>
              โœ… Discount applied: -$" + state.discountAmount.toFixed(2) + "
            </div>
          )}
        </div>
      )}
      
      {/* Total */}
      {state.items.length > 0 && (
        <div style={{
          backgroundColor: '#f8f9fa',
          padding: '15px',
          borderRadius: '5px',
          marginBottom: '15px'
        }}>
          <div>Subtotal: $" + state.total.toFixed(2) + "</div>
          {state.discountAmount > 0 && (
            <div>Discount: -$" + state.discountAmount.toFixed(2) + "</div>
          )}
          <div style={{ fontSize: '18px', fontWeight: 'bold' }}>
            Total: $" + finalTotal.toFixed(2) + "
          </div>
        </div>
      )}
      
      {/* Actions */}
      {state.items.length > 0 && (
        <div>
          <button
            style={{
              backgroundColor: '#28a745',
              color: 'white',
              padding: '10px 20px',
              border: 'none',
              borderRadius: '5px',
              marginRight: '10px'
            }}
          >
            Checkout
          </button>
          <button
            onClick={() => dispatch({ type: 'CLEAR_CART' })}
            style={{
              backgroundColor: '#dc3545',
              color: 'white',
              padding: '10px 20px',
              border: 'none',
              borderRadius: '5px'
            }}
          >
            Clear Cart
          </button>
        </div>
      )}
      
      {state.loading && (
        <div style={{ textAlign: 'center', marginTop: '15px' }}>
          ๐Ÿ”„ Loading...
        </div>
      )}
    </div>
  );
}

Benefits of this approach:

  • โœ… All state logic is centralized in the reducer
  • โœ… State updates are predictable and follow the same pattern
  • โœ… Easy to test reducer functions in isolation
  • โœ… Impossible to have inconsistent state (total always matches items)
  • โœ… Clear action names make it obvious what's happening

When to Use useReducer vs useState#

Use useState when:#

โœ… Simple, independent state (a boolean, a string, a number)
โœ… State changes are straightforward (toggle, set value)
โœ… No complex relationships between state pieces
โœ… Component state is small and easy to manage

// Good for useState
const [isOpen, setIsOpen] = useState(false);
const [name, setName] = useState('');
const [count, setCount] = useState(0);

Use useReducer when:#

โœ… Complex state object with multiple related properties
โœ… State transitions are complex or follow specific rules
โœ… Multiple ways to update the same state
โœ… You need predictable state management
โœ… Logic should be testable outside the component

// Good for useReducer
const [state, dispatch] = useReducer(formReducer, {
  values: {},
  errors: {},
  touched: {},
  isSubmitting: false
});

Real-World Example: Form Management#

Let's build a complex form with validation using useReducer:

// Form state and reducer
const initialFormState = {
  values: {
    name: '',
    email: '',
    password: '',
    confirmPassword: ''
  },
  errors: {},
  touched: {},
  isSubmitting: false,
  isValid: false
};

function formReducer(state, action) {
  switch (action.type) {
    case 'FIELD_CHANGE':
      const newValues = {
        ...state.values,
        [action.field]: action.value
      };
      
      // Validate field
      const newErrors = { ...state.errors };
      const validation = validateField(action.field, action.value, newValues);
      
      if (validation.isValid) {
        delete newErrors[action.field];
      } else {
        newErrors[action.field] = validation.message;
      }
      
      return {
        ...state,
        values: newValues,
        errors: newErrors,
        isValid: Object.keys(newErrors).length === 0
      };
      
    case 'FIELD_BLUR':
      return {
        ...state,
        touched: {
          ...state.touched,
          [action.field]: true
        }
      };
      
    case 'SUBMIT_START':
      return {
        ...state,
        isSubmitting: true
      };
      
    case 'SUBMIT_SUCCESS':
      return {
        ...initialFormState
      };
      
    case 'SUBMIT_ERROR':
      return {
        ...state,
        isSubmitting: false,
        errors: {
          ...state.errors,
          submit: action.payload
        }
      };
      
    case 'RESET_FORM':
      return initialFormState;
      
    default:
      throw new Error('Unknown action type: ' + action.type);
  }
}

// Validation helper
function validateField(fieldName, value, allValues) {
  switch (fieldName) {
    case 'name':
      if (!value.trim()) {
        return { isValid: false, message: 'Name is required' };
      }
      if (value.length < 2) {
        return { isValid: false, message: 'Name must be at least 2 characters' };
      }
      return { isValid: true };
      
    case 'email':
      const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;
      if (!value) {
        return { isValid: false, message: 'Email is required' };
      }
      if (!emailRegex.test(value)) {
        return { isValid: false, message: 'Please enter a valid email' };
      }
      return { isValid: true };
      
    case 'password':
      if (!value) {
        return { isValid: false, message: 'Password is required' };
      }
      if (value.length < 6) {
        return { isValid: false, message: 'Password must be at least 6 characters' };
      }
      return { isValid: true };
      
    case 'confirmPassword':
      if (!value) {
        return { isValid: false, message: 'Please confirm your password' };
      }
      if (value !== allValues.password) {
        return { isValid: false, message: 'Passwords do not match' };
      }
      return { isValid: true };
      
    default:
      return { isValid: true };
  }
}

// Form component
function RegistrationForm() {
  const [state, dispatch] = useReducer(formReducer, initialFormState);
  
  const handleFieldChange = (field, value) => {
    dispatch({
      type: 'FIELD_CHANGE',
      field,
      value
    });
  };
  
  const handleFieldBlur = (field) => {
    dispatch({
      type: 'FIELD_BLUR',
      field
    });
  };
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    
    if (!state.isValid) {
      // Mark all fields as touched to show errors
      Object.keys(state.values).forEach(field => {
        dispatch({ type: 'FIELD_BLUR', field });
      });
      return;
    }
    
    dispatch({ type: 'SUBMIT_START' });
    
    try {
      // Simulate API call
      await new Promise(resolve => setTimeout(resolve, 2000));
      
      // Simulate random success/failure
      if (Math.random() > 0.3) {
        dispatch({ type: 'SUBMIT_SUCCESS' });
        alert('Registration successful!');
      } else {
        throw new Error('Server error occurred');
      }
    } catch (error) {
      dispatch({
        type: 'SUBMIT_ERROR',
        payload: error.message
      });
    }
  };
  
  const getFieldError = (fieldName) => {
    return state.touched[fieldName] && state.errors[fieldName];
  };
  
  return (
    <div style={{ padding: '20px', maxWidth: '400px' }}>
      <h2>๐Ÿ“ Registration Form</h2>
      
      <form onSubmit={handleSubmit}>
        {/* Name field */}
        <div style={{ marginBottom: '15px' }}>
          <label style={{ display: 'block', marginBottom: '5px' }}>
            Name *
          </label>
          <input
            type="text"
            value={state.values.name}
            onChange={(e) => handleFieldChange('name', e.target.value)}
            onBlur={() => handleFieldBlur('name')}
            style={{
              width: '100%',
              padding: '8px',
              border: '1px solid ' + (getFieldError('name') ? 'red' : '#ddd'),
              borderRadius: '4px'
            }}
          />
          {getFieldError('name') && (
            <div style={{ color: 'red', fontSize: '14px', marginTop: '5px' }}>
              {getFieldError('name')}
            </div>
          )}
        </div>
        
        {/* Email field */}
        <div style={{ marginBottom: '15px' }}>
          <label style={{ display: 'block', marginBottom: '5px' }}>
            Email *
          </label>
          <input
            type="email"
            value={state.values.email}
            onChange={(e) => handleFieldChange('email', e.target.value)}
            onBlur={() => handleFieldBlur('email')}
            style={{
              width: '100%',
              padding: '8px',
              border: '1px solid ' + (getFieldError('email') ? 'red' : '#ddd'),
              borderRadius: '4px'
            }}
          />
          {getFieldError('email') && (
            <div style={{ color: 'red', fontSize: '14px', marginTop: '5px' }}>
              {getFieldError('email')}
            </div>
          )}
        </div>
        
        {/* Password field */}
        <div style={{ marginBottom: '15px' }}>
          <label style={{ display: 'block', marginBottom: '5px' }}>
            Password *
          </label>
          <input
            type="password"
            value={state.values.password}
            onChange={(e) => handleFieldChange('password', e.target.value)}
            onBlur={() => handleFieldBlur('password')}
            style={{
              width: '100%',
              padding: '8px',
              border: '1px solid ' + (getFieldError('password') ? 'red' : '#ddd'),
              borderRadius: '4px'
            }}
          />
          {getFieldError('password') && (
            <div style={{ color: 'red', fontSize: '14px', marginTop: '5px' }}>
              {getFieldError('password')}
            </div>
          )}
        </div>
        
        {/* Confirm Password field */}
        <div style={{ marginBottom: '20px' }}>
          <label style={{ display: 'block', marginBottom: '5px' }}>
            Confirm Password *
          </label>
          <input
            type="password"
            value={state.values.confirmPassword}
            onChange={(e) => handleFieldChange('confirmPassword', e.target.value)}
            onBlur={() => handleFieldBlur('confirmPassword')}
            style={{
              width: '100%',
              padding: '8px',
              border: '1px solid ' + (getFieldError('confirmPassword') ? 'red' : '#ddd'),
              borderRadius: '4px'
            }}
          />
          {getFieldError('confirmPassword') && (
            <div style={{ color: 'red', fontSize: '14px', marginTop: '5px' }}>
              {getFieldError('confirmPassword')}
            </div>
          )}
        </div>
        
        {/* Submit error */}
        {state.errors.submit && (
          <div style={{
            color: 'red',
            backgroundColor: '#ffebee',
            padding: '10px',
            borderRadius: '4px',
            marginBottom: '15px'
          }}>
            โŒ {state.errors.submit}
          </div>
        )}
        
        {/* Submit button */}
        <button
          type="submit"
          disabled={state.isSubmitting || !state.isValid}
          style={{
            width: '100%',
            padding: '12px',
            backgroundColor: state.isValid ? '#28a745' : '#6c757d',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
            fontSize: '16px',
            cursor: state.isValid ? 'pointer' : 'not-allowed'
          }}
        >
          {state.isSubmitting ? 'Creating Account...' : 'Create Account'}
        </button>
        
        <button
          type="button"
          onClick={() => dispatch({ type: 'RESET_FORM' })}
          style={{
            width: '100%',
            padding: '8px',
            backgroundColor: 'transparent',
            color: '#6c757d',
            border: '1px solid #6c757d',
            borderRadius: '4px',
            marginTop: '10px',
            cursor: 'pointer'
          }}
        >
          Reset Form
        </button>
      </form>
      
      {/* Debug info */}
      <details style={{ marginTop: '20px', fontSize: '12px' }}>
        <summary>Debug Info</summary>
        <pre>{JSON.stringify(state, null, 2)}</pre>
      </details>
    </div>
  );
}

This form demonstrates:

  • Complex state management with validation
  • Coordinated updates between values, errors, and touched fields
  • Predictable state transitions for all form actions
  • Easy testing since all logic is in the reducer

Combining useReducer with useContext#

For global state management, you can combine useReducer with useContext:

// Create context for global state
const AppStateContext = createContext();

// App state reducer
const initialAppState = {
  user: null,
  notifications: [],
  theme: 'light',
  settings: {
    emailNotifications: true,
    pushNotifications: false
  }
};

function appReducer(state, action) {
  switch (action.type) {
    case 'LOGIN':
      return {
        ...state,
        user: action.payload
      };
      
    case 'LOGOUT':
      return {
        ...state,
        user: null,
        notifications: []
      };
      
    case 'ADD_NOTIFICATION':
      return {
        ...state,
        notifications: [...state.notifications, action.payload]
      };
      
    case 'REMOVE_NOTIFICATION':
      return {
        ...state,
        notifications: state.notifications.filter(n => n.id !== action.payload)
      };
      
    case 'TOGGLE_THEME':
      return {
        ...state,
        theme: state.theme === 'light' ? 'dark' : 'light'
      };
      
    case 'UPDATE_SETTINGS':
      return {
        ...state,
        settings: {
          ...state.settings,
          ...action.payload
        }
      };
      
    default:
      throw new Error('Unknown action type: ' + action.type);
  }
}

// Provider component
function AppStateProvider({ children }) {
  const [state, dispatch] = useReducer(appReducer, initialAppState);
  
  return (
    <AppStateContext.Provider value={{ state, dispatch }}>
      {children}
    </AppStateContext.Provider>
  );
}

// Custom hook to use app state
function useAppState() {
  const context = useContext(AppStateContext);
  if (!context) {
    throw new Error('useAppState must be used within AppStateProvider');
  }
  return context;
}

// Component using global state
function UserDashboard() {
  const { state, dispatch } = useAppState();
  
  const handleLogin = () => {
    dispatch({
      type: 'LOGIN',
      payload: { id: 1, name: 'John Doe', email: 'john@example.com' }
    });
    
    dispatch({
      type: 'ADD_NOTIFICATION',
      payload: { id: Date.now(), message: 'Welcome back!', type: 'success' }
    });
  };
  
  const handleLogout = () => {
    dispatch({ type: 'LOGOUT' });
  };
  
  return (
    <div style={{
      padding: '20px',
      backgroundColor: state.theme === 'dark' ? '#333' : '#fff',
      color: state.theme === 'dark' ? '#fff' : '#333',
      minHeight: '100vh'
    }}>
      <h1>Dashboard</h1>
      
      {state.user ? (
        <div>
          <p>Welcome, {state.user.name}!</p>
          <button onClick={handleLogout}>Logout</button>
        </div>
      ) : (
        <button onClick={handleLogin}>Login</button>
      )}
      
      <button onClick={() => dispatch({ type: 'TOGGLE_THEME' })}>
        Switch to {state.theme === 'light' ? 'Dark' : 'Light'} Mode
      </button>
      
      {/* Notifications */}
      {state.notifications.map(notification => (
        <div key={notification.id} style={{
          backgroundColor: '#e3f2fd',
          padding: '10px',
          margin: '10px 0',
          borderRadius: '5px'
        }}>
          {notification.message}
          <button onClick={() => dispatch({
            type: 'REMOVE_NOTIFICATION',
            payload: notification.id
          })}>
            ร—
          </button>
        </div>
      ))}
    </div>
  );
}

// App component
function App() {
  return (
    <AppStateProvider>
      <UserDashboard />
    </AppStateProvider>
  );
}

Practice Exercise: Build a Todo App with Categories#

Try building a todo app with these features using useReducer:

Requirements:

  1. Add/edit/delete todos
  2. Mark todos as complete
  3. Organize todos by categories
  4. Filter todos (all, active, completed)
  5. Bulk actions (mark all complete, delete completed)

Starter structure:

const initialTodoState = {
  todos: [],
  categories: ['Work', 'Personal', 'Shopping'],
  filter: 'all', // 'all', 'active', 'completed'
  selectedCategory: 'all'
};

function todoReducer(state, action) {
  switch (action.type) {
    case 'ADD_TODO':
      // Your code here
      break;
    case 'TOGGLE_TODO':
      // Your code here
      break;
    case 'DELETE_TODO':
      // Your code here
      break;
    case 'SET_FILTER':
      // Your code here
      break;
    // Add more cases...
    default:
      throw new Error('Unknown action: ' + action.type);
  }
}

function TodoApp() {
  const [state, dispatch] = useReducer(todoReducer, initialTodoState);
  
  // Your component code here
}

Common useReducer Patterns#

Pattern 1: Action Creators#

// Instead of dispatching objects directly
dispatch({ type: 'ADD_TODO', payload: { text: 'Learn React', category: 'Work' } });

// Create action creator functions
const addTodo = (text, category) => ({
  type: 'ADD_TODO',
  payload: { text, category }
});

const toggleTodo = (id) => ({
  type: 'TOGGLE_TODO',
  payload: id
});

// Usage
dispatch(addTodo('Learn React', 'Work'));
dispatch(toggleTodo(123));

Pattern 2: Async Actions with useEffect#

function DataComponent() {
  const [state, dispatch] = useReducer(dataReducer, initialState);
  
  useEffect(() => {
    const fetchData = async () => {
      dispatch({ type: 'FETCH_START' });
      
      try {
        const data = await api.getData();
        dispatch({ type: 'FETCH_SUCCESS', payload: data });
      } catch (error) {
        dispatch({ type: 'FETCH_ERROR', payload: error.message });
      }
    };
    
    fetchData();
  }, []);
  
  return (
    <div>
      {state.loading && <p>Loading...</p>}
      {state.error && <p>Error: {state.error}</p>}
      {state.data && <DataDisplay data={state.data} />}
    </div>
  );
}

Pattern 3: Middleware-like Logging#

function loggingReducer(reducer) {
  return (state, action) => {
    console.log('Action:', action);
    console.log('Previous State:', state);
    const newState = reducer(state, action);
    console.log('New State:', newState);
    return newState;
  };
}

// Usage
const [state, dispatch] = useReducer(loggingReducer(myReducer), initialState);

What We've Learned#

Congratulations! You now understand:

โœ… When useReducer is better than useState
โœ… How to write reducer functions and actions
โœ… Complex state management patterns
โœ… Combining useReducer with useContext
โœ… Real-world applications like forms and shopping carts
โœ… Best practices and common patterns

Quick Recap Quiz#

Test your useReducer knowledge:

  1. What are the three things useReducer takes as arguments?
  2. What must every action object have?
  3. When should you choose useReducer over useState?
  4. What makes reducer functions "pure"?

Answers: 1) Reducer function, initial state, and optional init function, 2) A type property, 3) When state is complex or state transitions need to be predictable, 4) They don't mutate state and always return the same output for the same inputs

What's Next?#

In our next lesson, we'll learn about Custom Hooks - how to extract and reuse stateful logic across components. You'll discover how to create your own hooks that encapsulate complex functionality and make your components cleaner and more reusable.

useReducer provides the foundation for building powerful custom hooks that manage complex state!

Previous

useEffect Hook

Next

Component Composition