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

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 Show Task Component

Let's first create our show tasks component. To do so, run the following command:

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

Inside ShowTask.jsx, paste the following contents:

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 inside 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. 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 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;

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 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 showTask = slug => {
34    history.push(`/tasks/${slug}/show`);
35  };
36
37  const updateTask = slug => {
38    history.push(`/tasks/${slug}/edit`);
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        <h1 className="text-xl leading-5 text-center">
57          You have no tasks assigned πŸ˜”
58        </h1>
59      </Container>
60    );
61  }
62
63  return (
64    <Container>
65      <ListTasks
66        data={tasks}
67        destroyTask={destroyTask}
68        updateTask={updateTask}
69        showTask={showTask}
70      />
71    </Container>
72  );
73};
74
75export default Dashboard;

Now let's pass in the handler functions for each action associated with 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, destroyTask, updateTask, showTask }) => {
5    return (
6        <Table
7          data={data}
8          destroyTask={destroyTask}
9          updateTask={updateTask}
10          showTask={showTask}
11        />
12    );
13};
14
15export 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, destroyTask, updateTask, 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
14                data={data}
15                destroyTask={destroyTask}
16                updateTask={updateTask}
17                showTask={showTask}
18              />
19            </table>
20          </div>
21        </div>
22      </div>
23    </div>
24  );
25};
26
27export default Table;

Then, replace 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.title}>
9          <td className="px-6 py-4 text-sm font-medium leading-5 text-bb-gray whitespace-no-wrap">
10            {rowData.title}
11          </td>
12          <td className="px-6 py-4 text-sm font-medium leading-5 text-bb-gray whitespace-no-wrap">
13            {rowData.user_id}
14          </td>
15          <td className="px-6 py-4 text-sm font-medium leading-5 text-right cursor-pointer">
16            <a
17              className="text-bb-purple"
18              onClick={() => showTask(rowData.slug)}
19            >Show</a>
20          </td>
21          <td className="px-6 py-4 text-sm font-medium leading-5 text-right cursor-pointer">
22            <a
23              className="text-yellow-600 hover:text-yellow-900"
24              onClick={() => updateTask(rowData.slug)}
25            >
26              Edit
27            </a>
28          </td>
29          <td className="px-6 py-4 text-sm font-medium leading-5 text-right cursor-pointer">
30            <a
31              className="text-bb-red text-opacity-70 hover:text-opacity-100"
32              onClick={() => destroyTask(rowData.slug)}
33            >
34              Delete
35            </a>
36          </td>
37        </tr>
38      ))}
39    </tbody>
40  );
41};
42
43TableRow.propTypes = {
44  data: PropTypes.array.isRequired,
45  destroyTask: PropTypes.func,
46  updateTask: PropTypes.func,
47};
48
49export 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, replace the contents of App.jsx with the following:

1import React, { useEffect, useState } from "react";
2import { initializeLogger } from "common/logger";
3import Dashboard from "components/Dashboard";
4import CreateTask from "components/Tasks/CreateTask";
5import ShowTask from "components/Tasks/ShowTask";
6import { registerIntercepts, setAuthHeaders } from "apis/axios";
7import { Route, Switch, BrowserRouter as Router } from "react-router-dom";
8import { ToastContainer } from "react-toastify";
9
10const App = () => {
11  const [loading, setLoading] = useState(true);
12
13  useEffect(() => {
14    registerIntercepts();
15    initializeLogger();
16    setAuthHeaders(setLoading);
17  }, []);
18
19  return (
20    <Router>
21      <ToastContainer />
22      <Switch>
23        <Route exact path="/tasks/:slug/show" component={ShowTask} />
24        <Route exact path="/tasks/create" component={CreateTask} />
25        <Route exact path="/dashboard" component={Dashboard} />
26      </Switch>
27    </Router>
28  );
29};
30
31export default App;
32

Now, let's go to the URL http://localhost:3000/dashboard 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 http://localhost:3000/dashboard 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"