Redux: Writing a Todo List Reducer (Toggling a Todo)

Dan Abramov
InstructorDan Abramov

Share this video with your friends

Send Tweet
Published 6 years ago
Updated 9 months ago

Learn how to implement toggling a todo in a todo list application reducer.

[00:00] In this lesson, we will continue creating the reducer for to-do list application. The only action that this reducer currently handles is called At-To-Do. We also created a test that makes sure that when the reducer is called with an empty array as a state and the at-to-do action, it returns an array with a single to do element.

[00:25] In this lesson, we will follow the same approach to implement another action called Toggle To-Do. We're going to start with a test again. This time, we're testing a different action and we have a different initial state. The state before calling the reducer now includes two different to-dos with ID zero and one. Notice how both of them have their completed fields set to false.

[00:55] Next, I declare the action. The action is an object with the type property which is a toggle to-do string and the ID of the to-do that I want to be toggled. I declare the state that I expect to receive after calling the reducer. It's pretty much the same as before calling the reducer. However, I expect the to-do with the ID specified in the action or one in this case. The change is completed field.

[01:25] The reducer must be a pure function. As a matter of precaution, I called reprise on the state and the action. Finally, just like in the previous lesson, I'm asserting that the result of calling my reducer with the state before and the action is going to be deeply equal to the state after.

[01:47] My test is a function, so I need to call it at the end of the file. If I run it, it fails because I have not implemented handling this action yet.

[01:58] I'm adding a new switch case to my reducer. I remember that I shouldn't change the original array, so I'm using the array map method to produce a new array.

[02:10] The function I pass as an argument will be called for every to-do. If it's not a to-do I'm looking for, I don't want to change it. I just return it as is. However, if the to-do is the one we want to toggle, I'm going to return a new object that has all the properties of the original to-do object thanks to the object's pred operator, but also an inverted value of the completed field.

[02:38] Now both of our tests run successfully. We have an implementation of the reducer that can add and toggle to-dos.

Omri Mor
Omri Mor
~ 4 years ago

Thanks for the videos Dan! Very informative and well structured. I try to write the tests first on my own before I watch you do it. Was surprised to see I came up with a solution different than yours. Want to post here for review to see if my solution misses edge cases or has performance issues. Relevant code below :

const todoReducer = (state = [], action) => {
    switch (action.type) {
        case "TOGGLE_TODO":
            return [
                ...state.slice(0, action.id),
                {...state[action.id], isCompleted: !state[action.id].isCompleted},
                ...state.slice(action.id + 1)
            ]
        default:
            return state;
    }
}

thanks!

dan entous
dan entous
~ 4 years ago

change the ids on the todos so they don’t follow a sequential pattern or match their position in the array.

Christian
Christian
~ 4 years ago

Here's a way you could implement immutable updates in a reducer with Immutable.js:

const { List, Record } = Immutable;

const Todo = Record({
  id: 0,
  text: "",
  completed: false
});

const todos = (state = List(), action) => {
  switch (action.type) {
    case "TOGGLE_TODO":
      const index = state.findIndex(todo => todo.id === action.id);
      return state.update(index, todo =>
        todo.update("completed", completed => !completed)
      );
    default:
      return state;
  }
};

const testToggleTodo = () => {
  const stateBefore = List([
    Todo({
      id: 0,
      text: "Learn Redux",
      completed: false
    }),
    Todo({
      id: 1,
      text: "Go shopping",
      completed: false
    })
  ]);
  const action = {
    type: "TOGGLE_TODO",
    id: 1
  };
  const stateAfter = List([
    Todo({
      id: 0,
      text: "Learn Redux",
      completed: false
    }),
    Todo({
      id: 1,
      text: "Go shopping",
      completed: true
    })
  ]);

  expect(todos(stateBefore, action)).toEqual(stateAfter);
};

testToggleTodo();
console.log("All tests passed.");

Because this code is looking up todos by id, a better data structure to store Todos in may be an immutable Map indexed by id.

The toggle_todo action could then be simplified to:

return state.update(action.id, todo =>
  todo.update("completed", completed => !completed)
);