Back
Chapters

Deleting task

Search icon
Search Book
⌘K

In the last chapter, we saw how to update a task. 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 HTTP request should be sent to the backend.

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

  • No notification should be displayed if a task is successfully deleted because from a UI perspective the result of this action will be visible and showing a notification on top of it will be an overkill in this case.

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 Table component.

  • 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 Edit component from Tasks folder.

Implementing destroy action in TasksController

Let's implement the destroy action in our TasksController.

Add the following lines of code into TasksController:

1class TasksController < ApplicationController
2  before_action :load_task!, only: %i[show update destroy]
3
4  # previous code if any
5
6  def destroy
7    @task.destroy!
8    respond_with_json
9  end
10
11  private
12
13    # previous code if any

Here, load_task! methods runs before the destroy action and will have fetched the task whose slug matches our request parameter's slug value.

We do not need to send a json along with the response when a task is deleted successfully that is why we are responding with the respond_with_json method which will send a response with an ok status and an empty json if nothing is passed as an argument.

Now, update the tasks resources in config/routes.rb file:

1resources :tasks, except: %i[new edit], param: :slug

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 lines 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 =>
8  axios.post("/tasks/", {
9    task: payload,
10  });
11
12const update = ({ slug, payload }) =>
13  axios.put(`/tasks/${slug}`, {
14    task: payload,
15  });
16
17const destroy = slug => axios.delete(`/tasks/${slug}`);
18
19const tasksApi = {
20  list,
21  show,
22  create,
23  update,
24  destroy,
25};
26
27export default tasksApi;

Open app/javascript/src/components/Dashboard/index.jsx and fully 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 Table from "components/Tasks/Table";
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 {
16        data: { tasks },
17      } = await tasksApi.list();
18      setTasks(tasks);
19      setLoading(false);
20    } catch (error) {
21      logger.error(error);
22      setLoading(false);
23    }
24  };
25
26  const destroyTask = async slug => {
27    try {
28      await tasksApi.destroy(slug);
29      await fetchTasks();
30    } catch (error) {
31      logger.error(error);
32    }
33  };
34
35  const showTask = slug => {
36    history.push(`/tasks/${slug}/show`);
37  };
38
39  useEffect(() => {
40    fetchTasks();
41  }, []);
42
43  if (loading) {
44    return (
45      <div className="w-screen h-screen">
46        <PageLoader />
47      </div>
48    );
49  }
50
51  if (either(isNil, isEmpty)(tasks)) {
52    return (
53      <Container>
54        <h1 className="text-xl leading-5 text-center">
55          You have no tasks assigned πŸ˜”
56        </h1>
57      </Container>
58    );
59  }
60
61  return (
62    <Container>
63      <Table data={tasks} destroyTask={destroyTask} showTask={showTask} />
64    </Container>
65  );
66};
67
68export default Dashboard;

Now, we need to pass down destroyTask function as props to TableRow component. To do so, update app/javascript/src/components/Tasks/Table/index.jsx with the following lines of code:

1import React from "react";
2import TableHeader from "./TableHeader";
3import TableRow from "./TableRow";
4
5const Table = ({ data, showTask, 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                destroyTask={destroyTask}
17              />
18            </table>
19          </div>
20        </div>
21      </div>
22    </div>
23  );
24};
25
26export default Table;

Now, we need to make use of the destroyTask function so that a click on the Delete button in the tasks table would delete the corresponding task.

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

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

Destroying all in batches

If we invoke destroy_all method on a list of thousands of records then it can cause db and memory issues.

The destroy_all method loads the entire relation and then iteratively destroys the records one by one which can blow the memory very easily. Moreover, if we are destroying 5000 records using the destroy_all method then it places the db in a transaction lock. So this will prevents any other db calls from being executed and will cause APIs to timeout during the time db is locked.

So while using destroy_all method we should prefer using batches, like so:

1Model.in_batches.destroy_all

This would ensure that the records are destroyed in a batched fashion so that the memory and API calls don't take any hit.

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"