React Reducer

A reducer is basically a function that takes in the current state and an action, and returns a new state based off of these arguments. The “action"" argument is normally an ojbect with a type property, and based on the type of this action the reducer can do conditional work. So you can return a different state based on the action type. Usually it has a default if no type is assigned to the action. Normally you see reducers used with switch case, instead of if else if, but either can be used. The reducer does not directly change the state, it returns a new state object. To avoid this, you can use the spread operator to create a new copy of the current state before modifying it: let myNew = […oldState];

In the vanilla JavaScript example below, blatantly stolen from Robin Wieruch, we have a personReducer JS file. We also have another file that uses the personReducer component and returns a new state based on what is based to the personReducer. The personReducer is passed two arguments: The current state, and an action. In this case the action is an object that has a type of “CHANGE LASTNAME” and what the last name should be changed too. The personReducer function uses a switch to check the action type, and returns a new state based of of it.

The action for the reducer in this case is an object that contains the type, and an optional payload of the last name. The type is mandatory. The payload can be an object with more items in it.

const personReducer = (person, action) => {
  switch (action.type) {
    case 'INCREASE_AGE':
      return { ...person, age: person.age + 1 };
    case 'CHANGE_LASTNAME':
      return { ...person, lastname: action.lastname };
    default:
      return person;
  }
};

//////
const initialState = {
  firstname: 'Liesa',
  lastname: 'Huppertz',
  age: 30,
};
 
const action = {
  type: 'CHANGE_LASTNAME',
  lastname: 'Wieruch',
};
 
const result = personReducer(initialState, action);
 
expect(result).to.equal({
  firstname: 'Liesa',
  lastname: 'Wieruch',
  age: 30,
});

Here we have another example stolen from Robin again:

const todoReducer = (state, action) => {
  switch (action.type) {
    case 'DO_TODO':
      return state.map(todo => {
        if (todo.id === action.id) {
          return { ...todo, complete: true };
        } else {
          return todo;
        }
      });
    case 'UNDO_TODO':
      return state.map(todo => {
        if (todo.id === action.id) {
          return { ...todo, complete: false };
        } else {
          return todo;
        }
      });
    default:
      return state;
  }
};
const todos = [
  {
    id: 'a',
    task: 'Learn React',
    complete: false,
  },
  {
    id: 'b',
    task: 'Learn Firebase',
    complete: false,
  },
];
 
const action = {
  type: 'DO_TODO',
  id: 'a',
};
 
const newTodos = todoReducer(todos, action);
 
console.log(newTodos);
// [
//   {
//     id: 'a',
//     task: 'Learn React',
//     complete: true,
//   },
//   {
//     id: 'b',
//     task: 'Learn Firebase',
//     complete: false,
//   },
// ]

React!

At last, onto React and reducers! React has a hook, useReducer(), just for this type of stuff. This cool hook takes a reducer function and the initial state and returns the current state along with a dispatch function.
When to use this hook instead of the useState hook? It’s better to implement the useReducer hook if the next state depends on the value of the previous state, such as a counter. useReducer is also often better if you are storing objects in your state and not just primitives, especially complex nested objects.

  • If you have a component with multiple setState() calls, it might be better to try and encapsulate them into a single reducer function.