Back
Chapters

Showing a task

Search icon
Search Book
⌘K

In the last chapter, we saw how to create a new task and save it to the database. Let's add a new feature to display the newly created task.

Features

These are the requirements of this feature:

  • A show button should be present for each task. User should be redirected to task details page on clicking the show button.

  • Task details page should only contain the task title for now. Below is a picture showing how UI would look like once implemented.

    Do not get overwhelmed by the edit and delete buttons as well as the comment section. We will cover those features later in this book.

Showing a task feature

Technical design

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

On the backend

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

  • We will add a show action in the TasksController. When the application receives the request to show a task, that request will be processed by the show action.

On the frontend

  • Add a GET API connector for fetching a task inside the tasks API collection.

  • Add a Show component which will receive the task slug using URL params. Show component will call the API to fetch a task and display the task details.

  • In the Dashboard component, add a new showTask function which will redirect the application to a new page based on the task slug using react-router-dom.

  • Pass down showTask function as a prop to the TableRow component through the Table component.

  • Add a new Route in the App component for the task page. This route should render the Show component.

Implementing show action in TasksController

Open /app/controllers/tasks_controller.rb and add the following lines of code:

1class TasksController < ApplicationController
2  def show
3    task = Task.find_by!(slug: params[:slug])
4    respond_with_json({ task: task })
5  end
6end

In the show action we are trying to find and respond with a task by filtering using the slug attribute.

Note that in the above action we have used the find_by! method which will raise an exception if no matching record is found in the database. We do not need to worry about handling the exception anymore as we have already handled that inside the ApplicationController.

After the task is loaded successfully, the respond_with_json method responds with an :ok status and the task json.

We have chosen the respond_with_json method over the respond_with_success method here because respond_with_success method also sends back a success message along with a json which gets displayed as a notification in the client-side. We do not want to send a success message in this case.

Building Show Component

Let's first create our task's show component:

1touch app/javascript/src/components/Tasks/Show.jsx

In Show.jsx, paste the following content:

1import React, { useState, useEffect } from "react";
2import { useParams } from "react-router-dom";
3
4import Container from "components/Container";
5import PageLoader from "components/PageLoader";
6import tasksApi from "apis/tasks";
7
8const Show = () => {
9  const [taskDetails, setTaskDetails] = useState([]);
10  const [pageLoading, setPageLoading] = useState(true);
11  const { slug } = useParams();
12
13  const fetchTaskDetails = async () => {
14    try {
15      const {
16        data: { task },
17      } = await tasksApi.show(slug);
18      setTaskDetails(task);
19    } catch (error) {
20      logger.error(error);
21    } finally {
22      setPageLoading(false);
23    }
24  };
25
26  useEffect(() => {
27    fetchTaskDetails();
28  }, []);
29
30  if (pageLoading) {
31    return <PageLoader />;
32  }
33
34  return (
35    <Container>
36      <h1 className="pb-3 pl-3 mt-3 mb-3 text-lg leading-5 text-bb-gray border-b border-bb-gray">
37        <span>Task Title : </span> {taskDetails?.title}
38      </h1>
39    </Container>
40  );
41};
42
43export default Show;

useParams hook is provided by the react-router-dom package and it returns an object of URL parameters as key/value pairs. For example, for /tasks/:task-slug, useParams will return the following object:

1{
2  ("task_slug");
3}

Let's now create an API route to handle our show request in app/javascript/src/apis/tasks.js.

Note: Until now we have been using highlighted lines of code to show what needs to be added to the codebase/component.

However, from here onwards, since we will be making a lot of changes and additions to each of the components, if we haven't highlighted any lines in code block, then the best way to stay in sync is to copy the whole code snippet and replace the corresponding component/file in your local project with that content, unless specified otherwise.

App.jsx will only require updates in most chapters, rather than full replacement.

This will help you from not missing out on any of the changes that needs to be reflected in your local project.

Now update tasks.js with the following content:

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 tasksApi = {
13  list,
14  show,
15  create,
16};
17
18export default tasksApi;

Now we will create a showTask handler function in the Dashboard and pass it down to the Table component. Fully replace app/javascript/src/components/Dashboard/index.jsx with the following content:

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 showTask = slug => {
27    history.push(`/tasks/${slug}/show`);
28  };
29
30  useEffect(() => {
31    fetchTasks();
32  }, []);
33
34  if (loading) {
35    return (
36      <div className="w-screen h-screen">
37        <PageLoader />
38      </div>
39    );
40  }
41
42  if (either(isNil, isEmpty)(tasks)) {
43    return (
44      <Container>
45        <h1 className="text-xl leading-5 text-center">
46          You have no tasks assigned πŸ˜”
47        </h1>
48      </Container>
49    );
50  }
51
52  return (
53    <Container>
54      <Table data={tasks} showTask={showTask} />
55    </Container>
56  );
57};
58
59export default Dashboard;

We need to pass down showTask function as props to TableRow component and attach it to an onClick event which will be fired upon clicking the show button.

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 }) => {
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 data={data} showTask={showTask} />
14            </table>
15          </div>
16        </div>
17      </div>
18    </div>
19  );
20};
21
22export default Table;

Then, update the content of TableRow.jsx with following lines:

1import React from "react";
2import PropTypes from "prop-types";
3
4import Tooltip from "components/Tooltip";
5
6const TableRow = ({ data, 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        </tr>
28      ))}
29    </tbody>
30  );
31};
32
33TableRow.propTypes = {
34  data: PropTypes.array.isRequired,
35  showTask: PropTypes.func,
36};
37
38export default TableRow;

Now we have completed adding the show action handler for a task and hooked it to the onClick event. The last step is to define a route inside of App.jsx to render our tasks show page.

But while importing the Show component in App.jsx we will be using the same namespace as in Create component which is components/Tasks.

Here we can add an index.js file to components/Tasks to reduce the number of import statements in App.jsx and also to make things look cleaner.

These conventions are documented in this chapter in our React Best Practices Book.

Create a new file index.js in app/javascript/src/components/Tasks and add the following lines:

1import CreateTask from "./Create";
2import ShowTask from "./Show";
3
4export { CreateTask, ShowTask };

Here, we have imported the Create and Show as CreateTask and ShowTask because outside the tasks namespace or inside App.jsx names like Create and Show can be very confusing and may also give errors as there can be multiple Create and Show from different folders(namespaces). Thus it's better to export them as CreateTask and ShowTask.

Now add the highlighted lines to App.jsx:

1// previous imports if any
2import Dashboard from "components/Dashboard";
3import { CreateTask, ShowTask } from "components/Tasks";
4import { ToastContainer } from "react-toastify";
5
6const App = () => {
7  // previous code if any
8  return (
9    <Router>
10      <ToastContainer />
11      <Switch>
12        <Route exact path="/tasks/:slug/show" component={ShowTask} />
13        <Route exact path="/tasks/create" component={CreateTask} />
14        <Route exact path="/dashboard" component={Dashboard} />
15      </Switch>
16    </Router>
17  );
18};
19
20export default App;

Now, let's go to the dashboard URL and click on the Show button, we might see that an error has occurred:

1Routing Error
2No route matches [GET] "/tasks/slug"

We are getting an error because the Rails router can't find any route matching the format /tasks/:slug for a GET request or more accurately since we haven't defined show action in task resources.

Let's solve this by adding that action into /config/routes.rb.

Note: It is possible that you're able to access the show page for an individual task even before updating the routes.rb file without encountering any routing error.

But the issue that you might notice is that the title of the task will remain empty as we are not able to fetch details from the backend due to absence of the corresponding API route for returning the title details.

Adding the route in the following step will allow us to view the show page with title of the task:

1resources :tasks, only: %i[index create show], param: :slug

Now if we visit dashboard URL and then click on Show button corresponding to a task, we will be able to see the details of that task.

Using Rails console to search tasks

Let's fire up the console once again using the rails console command:

1rails console
2Running via Spring preloader in process 25412
3Loading development environment (Rails 7.0.3.1)
4
5irb(main):001:0> Task.find_by(slug: "my-first-task")
6  Task Load (0.2ms)  SELECT  "tasks".* FROM "tasks" WHERE "tasks"."slug" = ? LIMIT ?  [["slug", "my-first-task"], ["LIMIT", 1]]
7=> #<Task id: 1, title: "My first task", created_at: "2019-02-04 13:34:04", updated_at: "2019-02-04 13:34:04", slug: "my-first-task">
8
9irb(main):002:0> Task.find_by(slug: "my-4th-task")
10  Task Load (0.4ms)  SELECT  "tasks".* FROM "tasks" WHERE "tasks"."slug" = ? LIMIT ?  [["slug", "my-4th-task"], ["LIMIT", 1]]
11=> #<Task id: 5, title: "My 4th task", created_at: "2019-02-04 15:14:26", updated_at: "2019-02-04 15:14:26", slug: "my-4th-task">
12
13irb(main):003:0> Task.find_by(slug: "my-7th-task")
14  Task Load (0.4ms)  SELECT  "tasks".* FROM "tasks" WHERE "tasks"."slug" = ? LIMIT ?  [["slug", "my-7th-task"], ["LIMIT", 1]]
15=> #nil

As shown above, we use the find_by method and pass it with any slug.

If an entry in the database exists with corresponding slug, then the record is fetched otherwise exception is returned.

You can use the find_by! method which will raise an ActiveRecord::RecordNotFound exception if no tasks with a matching slug is found in the database.

1irb(main):004:0> Task.find_by!(slug: "my-7th-task")
2  Task Load (0.2ms)  SELECT  "tasks".* FROM "tasks" WHERE "tasks"."slug" = ? LIMIT ?  [["slug", "my-7th-task"], ["LIMIT", 1]]
3=> # `find_by!': Couldn't find Task (ActiveRecord::RecordNotFound)

We can also use other attributes to look for a specific task using the where method.

Let's try with the title attribute:

1irb(main):005:0> Task.where(title: "My first task")
2  Task Load (0.8ms)  SELECT  "tasks".* FROM "tasks" WHERE "tasks"."title" = ? LIMIT ?  [["title", "My first task"], ["LIMIT", 11]]
3=> #<ActiveRecord::Relation [#<Task id: 1, title: "My first task", created_at: "2019-02-04 13:34:04", updated_at: "2019-02-04 13:34:04", slug: "my-first-task">]>
4
5irb(main):006:0> Task.where(title: "My 4th task")
6  Task Load (0.4ms)  SELECT  "tasks".* FROM "tasks" WHERE "tasks"."title" = ? LIMIT ?  [["title", "My 4th task"], ["LIMIT", 11]]
7=> #<ActiveRecord::Relation [#<Task id: 4, title: "My 4th task", created_at: "2019-02-04 15:11:42", updated_at: "2019-02-04 15:11:42", slug: "my-4th-task">, #<Task id: 5, title: "My 4th task", created_at: "2019-02-04 15:14:26", updated_at: "2019-02-04 15:14:26", slug: "my-4th-task-2">]>

Now let's commit these changes:

1git add -A
2git commit -m "Added show page for a task"