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.

  • Add a before_action callback to invoke load_task method which will load the requested task before show action is called.

On the frontend

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

  • Add a ShowTask component which will receive the task slug using URL params. ShowTask 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 ListTasks and Table components.

  • Add a new Route in the App component for the task page. This route should render the ShowTask 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    render status: :ok, json: { task: task }
5    rescue ActiveRecord::RecordNotFound => errors
6      render json: {errors: errors}, status: :not_found
7  end
8end

Instead of finding task separately in every action, we can create a load_task method which will run before certain actions.

1class TasksController < ApplicationController
2  before_action :load_task, only: [:show]
3
4  def show
5    render status: :ok, json: { task: @task }
6  end
7
8  private
9
10  def load_task
11    @task = Task.find_by_slug!(params[:slug])
12    rescue ActiveRecord::RecordNotFound => errors
13      render json: {errors: errors}
14  end
15end

Here, the load_task method will find the task before running the show action.

Building ShowTask Component

Let's first create our show tasks component.

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

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

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, 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) => axios.post("/tasks/", payload);
8
9const tasksApi = {
10  list,
11  show,
12  create,
13};
14
15export default tasksApi;

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

Now let's pass in the handler functions for showing a task to our ListTasks component. To do so, make the following changes:

1import React from "react";
2import Table from "./Table";
3
4const ListTasks = ({ data, showTask }) => {
5  return <Table data={data} showTask={showTask} />;
6};
7
8export default ListTasks;

Now, we need to pass down showTask 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 }) => {
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
4const TableRow = ({ data, destroyTask, updateTask, showTask }) => {
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 className="px-6 py-4 text-sm font-medium leading-5 text-bb-gray whitespace-no-wrap">
16            {rowData.user_id}
17          </td>
18          <td className="px-6 py-4 text-sm font-medium leading-5 text-right cursor-pointer">
19            <a
20              className="text-bb-purple"
21              onClick={() => showTask(rowData.slug)}
22            >
23              Show
24            </a>
25          </td>
26        </tr>
27      ))}
28    </tbody>
29  );
30};
31
32TableRow.propTypes = {
33  data: PropTypes.array.isRequired,
34  showTask: PropTypes.func,
35};
36
37export default TableRow;

Now we have completed adding the show action handler for a task and hooked it into the onClick event on the ListTasks component.

The last step is to define a route inside of App.jsx to render our tasks show page.

For that, add the highlighted lines to App.jsx:

1// previous imports if any
2import Dashboard from "components/Dashboard";
3import CreateTask from "components/Tasks/CreateTask";
4import ShowTask from "components/Tasks/ShowTask";
5import { ToastContainer } from "react-toastify";
6
7const App = () => {
8  // previous code if any
9  return (
10    <Router>
11      <ToastContainer />
12      <Switch>
13        <Route exact path="/tasks/:slug/show" component={ShowTask} />
14        <Route exact path="/tasks/create" component={CreateTask} />
15        <Route exact path="/dashboard" component={Dashboard} />
16      </Switch>
17    </Router>
18  );
19};
20
21export 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 that, we will be using the except keyword over only, since we only need to exclude two actions and include the rest.

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, except: %i[new edit], 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 5.2.2)
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]]
15Traceback (most recent call last):
16        1: from (irb):3
17ActiveRecord::RecordNotFound (Couldn't find Task with 'slug'='my-7th-task')

As shown above, we use the find_by_slug! 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.

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):004: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):005: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"
⌘K
    to navigateEnterto select Escto close
    Previous
    Next