Logic for Modifying Tasks
Finalize the task completion by testing and implementing relevant logic in React
We'll cover the following
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:
- Accept a callback function via prop (i.e.,
onToggleTask
). - 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.