Logic for Modifying Tasks

Finalize the task completion by testing and implementing relevant logic in React

Where we left off

In the previous exercise, we made the Task component forward user input to a callback function. Now, we will do the same for TaskList.

If you recall, we currently store tasks in state of the App component:

const [tasks, setTasks] = useState([]);
const handleNewTask = (task) => setTasks([...tasks, task]);

New handler functions

Do you notice anything off about the snippet above? When developing the Task component we decided to store tasks as a JS object like this:

{
  label: 'Do this',
  completed: false
}

We need to change the handleNewTask to account for that:

const handleNewTask = (task) => setTasks([...tasks, {completed: false, label: task}]);

We will also need a handler function to toggle the task as completed or not. Here is what I came up with:

const handleToggleTask = (taskIdx) => {
  const newTasks = [...tasks];
  newTasks[taskIdx] = {...newTasks[taskIdx], completed: !newTasks[taskIdx].completed};
  setTasks(newTasks);
};

As you can see, this function accepts the task index as its argument. Then we go ahead and make a shallow copy of the tasks list. In it, we make another shallow copy of the task we are about to modify and toggle its completed key. Lastly, we update the tasks using the setTasks function. This is done for immutability purposes, to make sure nothing breaks spontaneously.

Do you remember why we do not test these functions with unit tests? These are bits of core logic that (1) are tested by integration tests and (2) do not work in isolation.

TaskList tests

Now, we are ready to write some tests for TaskList and make way for implementation. As we just wrote, the handler function to toggle tasks wants the task index as an argument, and that is precisely what we are about to test. We want TaskList to:

  1. Accept a callback function via prop (i.e., onToggleTask).
  2. Call it with the task’s index each time a user clicks on it.

Firstly, we will create the test in TaskList.test.js:

it('must fire onToggle callback', () => {

});

Let’s define two sample tasks to render:

const tasks = [
  {label: 'Do this', completed: false},
  {label: 'Do that', completed: true},
];

We will also need a mock callback similar to the last exercise:

const mockOnToggle = jest.fn();

Now, combine these to render the TaskList:

render(<TaskList tasks={tasks} onToggleTask={mockOnToggle} />);

To simulate user input, we will get a list of all tasks. Click on the second one:

const renderedTasks = tasks.map(task => screen.getByText(task.label));
fireEvent.click(renderedTasks[1]);

Lastly, we can assert that the mock callback was called like in the last exercise:

expect(mockOnToggle).toHaveBeenCalled();

However, this time, the assertion is not particularly useful. We want the callback function to be called with the task index (1 in this case), and the assertion does not care for that. Luckily, there is another function supplied by Jest:

expect(mockOnToggle).toHaveBeenCalledWith(1);

This assertion will pass (if and only if) the callback was called with 1 as an argument. If you run the tests right now, it would fail because TaskList does not call the callback at all! Let’s fix that now.

TaskList implementation

If you recall, we wrote the onToggle prop for the Task component specifically to forward user interaction up the component tree. To make use of it, unpack the onToggleTask prop, and pass it to Task:

const TaskList = ({tasks, onToggleTask}) => {
  return (
    <ul>
      {tasks.map((task) => 
        <Task key={task.label} task={task} onToggle={onToggleTask} />
      )}
    </ul>
  );
};

Now, every click on Task will call the onToggleTask callback, just like we wanted. The last thing to take care of is the task index. We need to pass it to the onToggleTask function. There are many ways to do that, but I did it like this:

const TaskList = ({tasks, onToggleTask}) => {
    return (
        <ul>
            {tasks.map((task, idx) =>
                <Task key={task.label} task={task} onToggle={() => onToggleTask(idx)} />
            )}
        </ul>
    );
};

If you do not understand where idx comes from, it is the magic of the .map function. It will always pass the index of the element as the second argument for the callback function.

The very last step to make all of this work is to pass in the handler function that we wrote earlier to TaskList, which now knows what to do with it (in App.js):

return (
    <div>
      <TaskInput onSubmit={handleNewTask}/>
      <TaskList tasks={tasks} onToggleTask={handleToggleTask}/>
    </div>
);

Here is the whole project so far for reference:

Get hands-on with 1200+ tech skills courses.