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.

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 }
12    end
13  end
14
15  private
16
17  ...

Here, load_task is the method that we had defined in show action which uses params[:slug] which contains the slug of 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 data={tasks} destroyTask={destroyTask}
57        updateTask={updateTask} showTask={showTask} />
58      </Container>
59    );
60  }
61
62  return (
63    <Container>
64      <h1 className="text-xl leading-5 text-center">
65      You have no tasks assigned πŸ˜”</h1>
66    </Container>
67  );
68};
69
70export default Dashboard;
71

Now that we have created a function to handle deletion of task and passed it to ListTasks component as props, we need to make use of that 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            {
8                data.map(rowData => (
9                    <tr key={rowData.title}>
10                        <td className="px-6 py-4 text-sm font-medium
11                         leading-5 text-gray-900 whitespace-no-wrap">
12                            {rowData.title}
13                        </td>
14                        <td className="px-6 py-4 text-sm font-medium
15                         leading-5 text-gray-900 whitespace-no-wrap">
16                            {rowData.user_id}
17                        </td>
18                        <td className="px-6 py-4 text-sm font-medium
19                         leading-5 text-right cursor-pointer">
20                            <a className="text-indigo-600
21                            hover:text-indigo-900"
22                            onClick={() => updateTask(rowData.slug)}
23                            >Edit</a>
24                        </td>
25                        <td className="px-6 py-4 text-sm font-medium
26                         leading-5 text-right cursor-pointer">
27                            <a className="text-red-500
28                            hover:text-red-700"
29                            onClick={() => destroyTask(rowData.slug)}
30                            >Delete</a>
31                        </td>
32                    </tr>
33                ))
34            }
35        </tbody>
36    );
37};
38
39TableRow.propTypes = {
40    data: PropTypes.array.isRequired,
41    destroyTask: PropTypes.func,
42    updateTask: PropTypes.func
43};
44
45export 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"