In the last chapter, we saw how to update a task to the database. In this chapter we'll see how to delete a task.

Features

These are the basic requirements of the feature.

  • A delete button should be present for all tasks.

  • Upon clicking the delete button, a DELETE request should be sent to the backend.

  • Once a response is received for the DELETE request a notification should display with either a success message or error depending upon the response status.

  • User should be redirected to the Dashboard from the task deletion page if task is successfully deleted.

Technical design

To implement this feature, we need to introduce the following changes

On the backend

  • Add a destroy route for tasks RESTful resources in the routes file.

  • Add a destroy action in the TasksController. When application receives a DELETE request to delete a task, it will be processed by the delete action.

  • Update the before_action callback in the TasksController to call the load_task method before destroy action is called.

On the frontend

  • Add a DELETE request API connector for deleting a task.

  • Update the Dashboard component and add a deleteTask function which will handle the logic to delete the task.

  • Pass deleteTask function as a prop to the TableRow component through ListTasks and Table components.

  • Update the TableRow component and add a delete button which will call the deleteTask function received through props.

  • Add a route in App component for rendering the EditTask component.

Implementing destroy action in TasksController

Let's implement the destroy action to our TasksController.

1class TasksController < ApplicationController
2  before_action :load_task, only: %i[show update destroy]
3
4 ...
5
6  def destroy
7    if @task.destroy
8      render status: :ok, json: { notice: 'Successfully deleted task.' }
9    else
10      render status: :unprocessable_entity, json: { errors:
11      @task.errors.full_messages.to_sentence }
12    end
13  end
14
15  private
16
17  ...

Here, load_task is the method that we have already created. It uses params[:slug] which contains the slug of the task to load the task to be deleted.

If the task deletion is successful, status ok will be returned. On failure we will return status unprocessable_entity.

Handling task deletion

When user clicks on the "Delete" button then we need to handle that click. Then send a request to the server to delete the task. Let's handle it.

First, let's add an API for deleting a task. To do so, add the following line to app/javascript/src/apis/tasks.js:

1import axios from "axios";
2
3const list = () => axios.get("/tasks");
4
5const show = (slug) => axios.get(`/tasks/${slug}`);
6
7const create = (payload) => axios.post("/tasks/", payload);
8
9const update = (slug, payload) => axios.put(`/tasks/${slug}`, payload);
10
11const destroy = (slug) => axios.delete(`/tasks/${slug}`);
12
13const tasksApi = {
14  list,
15  show,
16  create,
17  update,
18  destroy,
19};
20
21export default tasksApi;

Open app/javascript/src/components/Dashboard/index.jsx and replace the content of the file with the code shown below.

1import React, { useState, useEffect } from "react";
2import { isNil, isEmpty, either } from "ramda";
3
4import Container from "components/Container";
5import ListTasks from "components/Tasks/ListTasks";
6import tasksApi from "apis/tasks";
7import PageLoader from "components/PageLoader";
8
9const Dashboard = ({ history }) => {
10  const [tasks, setTasks] = useState([]);
11  const [loading, setLoading] = useState(true);
12
13  const fetchTasks = async () => {
14    try {
15      const response = await tasksApi.list();
16      setTasks(response.data.tasks);
17      setLoading(false);
18    } catch (error) {
19      logger.error(error);
20      setLoading(false);
21    }
22  };
23
24  const destroyTask = async (slug) => {
25    try {
26      await tasksApi.destroy(slug);
27      await fetchTasks();
28    } catch (error) {
29      logger.error(error);
30    }
31  };
32
33  const updateTask = (slug) => {
34    history.push(`/tasks/${slug}/edit`);
35  };
36
37  const showTask = (slug) => {
38    history.push(`/tasks/${slug}/show`);
39  };
40
41  useEffect(() => {
42    fetchTasks();
43  }, []);
44
45  if (loading) {
46    return (
47      <div className="w-screen h-screen">
48        <PageLoader />
49      </div>
50    );
51  }
52
53  if (!either(isNil, isEmpty)(tasks)) {
54    return (
55      <Container>
56        <ListTasks
57          data={tasks}
58          destroyTask={destroyTask}
59          updateTask={updateTask}
60          showTask={showTask}
61        />
62      </Container>
63    );
64  }
65
66  return (
67    <Container>
68      <h1 className="text-xl leading-5 text-center">
69        You have no tasks assigned 😔
70      </h1>
71    </Container>
72  );
73};
74
75export default Dashboard;

Now let's pass in the handler function for deleteTask action to our ListTasks component. To do so, make the following changes:

1import React from "react";
2import Table from "./Table";
3
4const ListTasks = ({ data, updateTask, showTask, destroyTask }) => {
5  return (
6    <Table
7      data={data}
8      showTask={showTask}
9      updateTask={updateTask}
10      destroyTask={destroyTask}
11    />
12  );
13};
14
15export default ListTasks;

Now, we need to pass down updateTask function as props to TableRow component. To do so, update Table.jsx with the following lines of code:

1import React from "react";
2import TableHeader from "./TableHeader";
3import TableRow from "./TableRow";
4
5const Table = ({ data, showTask, updateTask, destroyTask }) => {
6  return (
7    <div className="flex flex-col">
8      <div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
9        <div className="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
10          <div className="overflow-hidden border-b border-bb-gray-200 shadow sm:rounded-lg">
11            <table className="min-w-full divide-y divide-gray-200">
12              <TableHeader />
13              <TableRow
14                data={data}
15                showTask={showTask}
16                updateTask={updateTask}
17                destroyTask={destroyTask}
18              />
19            </table>
20          </div>
21        </div>
22      </div>
23    </div>
24  );
25};
26
27export default Table;

Now, we need to make use of the deleteTask function so that a click on the Delete button in Table.jsx would delete the corresponding task.

To do so, go to app/javascript/src/components/Tasks/Table/TableRow.jsx and add the following line:

1import React from "react";
2import PropTypes from "prop-types";
3
4const TableRow = ({ data, destroyTask, updateTask }) => {
5  return (
6    <tbody className="bg-white divide-y divide-gray-200">
7      {data.map((rowData) => (
8        <tr key={rowData.id}>
9          <td
10            className="block w-64 px-6 py-4 text-sm font-medium
11            leading-8 text-bb-purple capitalize truncate"
12          >
13            {rowData.title}
14          </td>
15          <td
16            className="px-6 py-4 text-sm font-medium
17            leading-5 text-gray-900 whitespace-no-wrap"
18          >
19            {rowData.user_id}
20          </td>
21          <td className="px-6 py-4 text-sm font-medium leading-5 text-right cursor-pointer">
22            <a
23              className="text-bb-purple"
24              onClick={() => showTask(rowData.slug)}
25            >
26              Show
27            </a>
28          </td>
29          <td
30            className="px-6 py-4 text-sm font-medium
31            leading-5 text-right cursor-pointer"
32          >
33            <a
34              className="text-indigo-600
35              hover:text-indigo-900"
36              onClick={() => updateTask(rowData.slug)}
37            >
38              Edit
39            </a>
40          </td>
41          <td
42            className="px-6 py-4 text-sm font-medium
43            leading-5 text-right cursor-pointer"
44          >
45            <a
46              className="text-red-500
47              hover:text-red-700"
48              onClick={() => destroyTask(rowData.slug)}
49            >
50              Delete
51            </a>
52          </td>
53        </tr>
54      ))}
55    </tbody>
56  );
57};
58
59TableRow.propTypes = {
60  data: PropTypes.array.isRequired,
61  destroyTask: PropTypes.func,
62  updateTask: PropTypes.func,
63};
64
65export default TableRow;

Deleting a task

So let's go through the Destroy flow of our task.

When we click the delete button, the onClick function uses the destroyTask() for making the API call and handles the task deletion provided the correct route and slug of the task. The control then goes to the router and router directs the control to destroy action of the TasksController. Once it's done, we again fetch the list of tasks from the db, because the database should be the source of truth for the data we show in UI in all cases.

1git add -A
2git commit -m "Added ability to delete a task"
⌘K
    to navigateEnterto select Escto close
    Previous
    Next