⚙️ useReducer Hook

Manage Complex State Logic

What is useReducer?

useReducer is an alternative to useState for managing complex state logic. It's similar to Redux - you dispatch actions to a reducer function that returns the new state.

When to Use useReducer:

  • Complex state with multiple sub-values
  • Next state depends on previous state
  • Multiple ways to update the same state
  • Want to centralize state logic

📚 Basic Syntax

import { useReducer } from 'react';

// Reducer function
function reducer(state, action) {
  switch (action.type) {
    case 'ACTION_TYPE':
      return { ...state, /* changes */ };
    default:
      return state;
  }
}

// Component
function Component() {
  const [state, dispatch] = useReducer(reducer, initialState);
  
  // Dispatch actions
  dispatch({ type: 'ACTION_TYPE', payload: value });
  
  return <div>{state.value}</div>;
}

🎯 Simple Counter Example

// Reducer function
function counterReducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };
    case 'RESET':
      return { count: 0 };
    case 'SET':
      return { count: action.payload };
    default:
      return state;
  }
}

function Counter() {
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });
  
  return (
    <div>
      <h2>Count: {state.count}</h2>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
      <button onClick={() => dispatch({ type: 'RESET' })}>Reset</button>
      <button onClick={() => dispatch({ type: 'SET', payload: 100 })}>Set 100</button>
    </div>
  );
}

📝 Todo List Example

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

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
      };
    
    case 'CLEAR_COMPLETED':
      return {
        ...state,
        todos: state.todos.filter(todo => !todo.completed)
      };
    
    default:
      return state;
  }
}

function TodoApp() {
  const [state, dispatch] = useReducer(todoReducer, initialState);
  const [input, setInput] = useState('');
  
  const handleSubmit = (e) => {
    e.preventDefault();
    if (input.trim()) {
      dispatch({ type: 'ADD_TODO', payload: input });
      setInput('');
    }
  };
  
  // Filter todos based on current filter
  const filteredTodos = state.todos.filter(todo => {
    if (state.filter === 'active') return !todo.completed;
    if (state.filter === 'completed') return todo.completed;
    return true;  // 'all'
  });
  
  return (
    <div>
      <form onSubmit={handleSubmit}>
        <input 
          value={input}
          onChange={e => setInput(e.target.value)}
          placeholder="Add todo"
        />
        <button type="submit">Add</button>
      </form>
      
      <div>
        <button onClick={() => dispatch({ type: 'SET_FILTER', payload: 'all' })}>
          All
        </button>
        <button onClick={() => dispatch({ type: 'SET_FILTER', payload: 'active' })}>
          Active
        </button>
        <button onClick={() => dispatch({ type: 'SET_FILTER', payload: 'completed' })}>
          Completed
        </button>
      </div>
      
      <ul>
        {filteredTodos.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 })}>
              ×
            </button>
          </li>
        ))}
      </ul>
      
      <button onClick={() => dispatch({ type: 'CLEAR_COMPLETED' })}>
        Clear Completed
      </button>
    </div>
  );
}

📝 Form with Validation

const initialState = {
  values: {
    email: '',
    password: '',
    confirmPassword: ''
  },
  errors: {},
  touched: {},
  isSubmitting: false
};

function formReducer(state, action) {
  switch (action.type) {
    case 'SET_FIELD':
      return {
        ...state,
        values: {
          ...state.values,
          [action.field]: action.value
        }
      };
    
    case 'SET_ERROR':
      return {
        ...state,
        errors: {
          ...state.errors,
          [action.field]: action.error
        }
      };
    
    case 'TOUCH_FIELD':
      return {
        ...state,
        touched: {
          ...state.touched,
          [action.field]: true
        }
      };
    
    case 'START_SUBMIT':
      return {
        ...state,
        isSubmitting: true
      };
    
    case 'SUBMIT_SUCCESS':
      return initialState;  // Reset form
    
    case 'SUBMIT_ERROR':
      return {
        ...state,
        isSubmitting: false,
        errors: action.errors
      };
    
    default:
      return state;
  }
}

function SignupForm() {
  const [state, dispatch] = useReducer(formReducer, initialState);
  
  const validateField = (field, value) => {
    let error = '';
    
    if (field === 'email') {
      if (!value) error = 'Email required';
      else if (!/\S+@\S+\.\S+/.test(value)) error = 'Email invalid';
    } else if (field === 'password') {
      if (!value) error = 'Password required';
      else if (value.length < 6) error = 'Password must be 6+ characters';
    } else if (field === 'confirmPassword') {
      if (value !== state.values.password) error = 'Passwords do not match';
    }
    
    dispatch({ type: 'SET_ERROR', field, error });
  };
  
  const handleChange = (field, value) => {
    dispatch({ type: 'SET_FIELD', field, value });
    if (state.touched[field]) {
      validateField(field, value);
    }
  };
  
  const handleBlur = (field) => {
    dispatch({ type: 'TOUCH_FIELD', field });
    validateField(field, state.values[field]);
  };
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    
    // Validate all fields
    Object.keys(state.values).forEach(field => {
      validateField(field, state.values[field]);
    });
    
    // Check if any errors
    if (Object.values(state.errors).some(error => error)) {
      return;
    }
    
    dispatch({ type: 'START_SUBMIT' });
    
    try {
      await fetch('/api/signup', {
        method: 'POST',
        body: JSON.stringify(state.values)
      });
      dispatch({ type: 'SUBMIT_SUCCESS' });
      alert('Signup successful!');
    } catch (error) {
      dispatch({ type: 'SUBMIT_ERROR', errors: { submit: 'Signup failed' } });
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <input
          type="email"
          value={state.values.email}
          onChange={e => handleChange('email', e.target.value)}
          onBlur={() => handleBlur('email')}
          placeholder="Email"
        />
        {state.touched.email && state.errors.email && (
          <span className="error">{state.errors.email}</span>
        )}
      </div>
      
      <div>
        <input
          type="password"
          value={state.values.password}
          onChange={e => handleChange('password', e.target.value)}
          onBlur={() => handleBlur('password')}
          placeholder="Password"
        />
        {state.touched.password && state.errors.password && (
          <span className="error">{state.errors.password}</span>
        )}
      </div>
      
      <div>
        <input
          type="password"
          value={state.values.confirmPassword}
          onChange={e => handleChange('confirmPassword', e.target.value)}
          onBlur={() => handleBlur('confirmPassword')}
          placeholder="Confirm Password"
        />
        {state.touched.confirmPassword && state.errors.confirmPassword && (
          <span className="error">{state.errors.confirmPassword}</span>
        )}
      </div>
      
      

🎯 useReducer vs useState

useState useReducer
Simple state Complex state with multiple values
Few state updates Many different ways to update
Logic scattered Logic centralized in reducer
Easy to test component Easy to test reducer (pure function)
Quick to write More boilerplate

⚠️ Best Practices

1. Use Action Constants

// ✅ Good - prevents typos
const ACTIONS = {
  ADD_TODO: 'ADD_TODO',
  TOGGLE_TODO: 'TOGGLE_TODO',
  DELETE_TODO: 'DELETE_TODO'
};

dispatch({ type: ACTIONS.ADD_TODO, payload: text });

2. Keep Reducer Pure

// ❌ Bad - side effects in reducer
function reducer(state, action) {
  fetch('/api/save', { body: state });  // Don't do this!
  return newState;
}

// ✅ Good - pure function
function reducer(state, action) {
  return newState;  // Only return new state
}

// Do side effects in useEffect
useEffect(() => {
  fetch('/api/save', { body: state });
}, [state]);

3. Return New Objects

// ❌ Bad - mutating state
function reducer(state, action) {
  state.count++;  // Don't mutate!
  return state;
}

// ✅ Good - return new object
function reducer(state, action) {
  return { ...state, count: state.count + 1 };
}

🎯 Key Takeaways