Real Example: Registration Form

Let's see how useReducer saves us when form fields start " communicating " with each other.

1. Reducer Structure

The most popular way to write a reducer is through the switch construct. This allows easily seeing all possible " scenarios " of events.

const initialState = {
  username: '',
  email: '',
  password: '',
  error: null,
  isSubmitting: false
};

function formReducer(state, action) {
  switch (action.type) {
    case 'FIELD_CHANGE':
      return { 
        ...state, 
        [action.field]: action.value, // Dynamic field name
        error: null 
      };
    case 'SUBMIT_START':
      return { ...state, isSubmitting: true };
    case 'SUBMIT_ERROR':
      return { ...state, isSubmitting: false, error: action.message };
    case 'RESET':
      return initialState; // Simplest way to clear form
    default:
      return state;
  }
}

2. What Does This Teach Us?

  1. Atomicity: Ever forgot to turn off the loading " spinner " on error? With useReducer this is impossible. In the SUBMIT_ERROR case we explicitly write: isSubmitting: false. All state updates in one go.
  2. Scalability: If tomorrow you need to add a " Confirm password " field, you don't need to create a new useState. You just handle another FIELD_CHANGE.
  3. Debugging Ease: Put console.log(action) at the start of the reducer. Now in the console you'll see the exact history of your application's life: " Pressed " , " Typed " , " Error " , " Reset " .

3. When NOT to Use useReducer?

Don't overcomplicate!

  • If you have a search field or simple toggle " Dark/Light theme " — keep useState.
  • If you don't have complex logic (e.g., one field doesn't depend on another) — useState will be faster and clearer for colleagues.

Remember: useReducer is heavy artillery. Use it when you feel useState is turning into " spaghetti code " .