Learn Ruby on Rails Book

Reusable components

In React, a reusable component is a piece of UI that can be used in various parts of an application to build more than one UI instance.

For instance, we can have a Button component that displays different texts on different pages. Generally speaking, we can make a component more reusable by turning it from more specific to more generic.

Features

Let us create the following components in this chapter. We will make each of these components reusable with the help of props:

  • Navbar
  • Table
  • Input
  • Button
  • PageLoader

Navbar and table components

Input and button components

Technical design

In this chapter we will make the following changes:

  • We will be creating a reusable Button component. In our Button component, we will provide options to update the type of the button and the handler for the button events. Along with that, we will also provide a loading option, which comes in handy when submitting a form.

  • While naming the components, we will follow PascalCase for naming both the exported component as well as the filename.

  • Create a Navbar Component that will show us nav items like todos and logout.

  • We will add remixicon package to the project, which will allow us to use icons by specifying the icon code in the class names of an HTML tag.

  • We will be adding a reusable Input component. In that we will have options to give the type of the input and the handler for the input change events. Along with that, we will also provide a label and placeholder options.

  • There are cases where the page can't be rendered yet due to invalid data or due to not receiving a response from an API call. In such situations to let the user know that the page is loading, we will create a PageLoader component.

  • We need a visually understandable structure for listing tasks along with their details and corresponding icons. For that let's create a reusable Table component that will consist of TableRow's and a TableHeader.

We are now ready to move to the implementation part. Let us dive in.

Custom Tailwind colors

We will be using, custom defined color classes to style our components. In order to do so, add the following lines of code to tailwind.config.js:

1module.exports = {
2  future: {
3    // removeDeprecatedGapUtilities: true,
4    // purgeLayersByDefault: true,
5  },
6  purge: [],
7  theme: {
8    extend: {
9      colors: {
10        "bb-purple": "#5469D4",
11        "bb-env": "#F1F5F9",
12        "bb-border": "#E4E4E7",
13        "bb-gray-700": "#37415",
14        "bb-gray-600": "#4B5563",
15        "bb-red": "#F56565",
16        "bb-green": "#31C48D",
17        "bb-yellow": "#F6B100",
18        "nitro-gray-800": "#1F2937",
19      },
20      boxShadow: {
21        "custom-box-shadow": "10px 10px 5px 200px rgba(0,0,0,1)",
22      },
23    },
24  },
25  variants: {},
26  plugins: [],
27};

Button component

Let's first create a Button component:

1mkdir -p app/javascript/src/components
2touch app/javascript/src/components/Button.jsx

In Button.jsx paste the following content:

1import React from "react";
2import PropTypes from "prop-types";
3
4const Button = ({ type = "button", buttonText, onClick, loading }) => {
5  return (
6    <div className="mt-6">
7      <button
8        type={type}
9        onClick={onClick}
10        className="relative flex justify-center w-full px-4 py-2
11        text-sm font-medium leading-5 text-white transition duration-150
12         ease-in-out bg-bb-purple border border-transparent rounded-md
13         group hover:bg-opacity-90 focus:outline-none"
14      >
15        {loading ? "Loading..." : buttonText}
16      </button>
17    </div>
18  );
19};
20
21Button.propTypes = {
22  type: PropTypes.string,
23  buttonText: PropTypes.string,
24  loading: PropTypes.bool,
25  onClick: PropTypes.func,
26};
27export default Button;

To import Button component in any of the views, we need to add the following line at the top of the file:

1import Button from "components/Button";

Let's create a NavItem reusable component:

1mkdir -p app/javascript/src/components/NavBar/
2touch app/javascript/src/components/NavBar/NavItem.jsx

In NavItem.jsx, paste the following content:

1import React from "react";
2import { Link } from "react-router-dom";
3
4const NavItem = ({ iconClass, name, path }) => {
5  return (
6    <Link
7      to={path}
8      className="inline-flex items-center px-1 pt-1 mr-3
9      font-semibold text-sm leading-5
10      text-indigo-500 hover:text-indigo-500"
11    >
12      {iconClass && <i className={`${iconClass} text-bb-purple`}></i>}
13      {name}
14    </Link>
15  );
16};
17
18export default NavItem;

To create a NavBar component which will make use of the NavItems, run the following command:

1touch app/javascript/src/components/NavBar/index.jsx

It's named index.jsx is because it's the root file which will be auto-imported in scenarios where we import like say import NavBar from '../NavBar'.

In index.jsx, paste the following content:

1import React from "react";
2import NavItem from "./NavItem";
3
4const NavBar = () => {
5  return (
6    <nav className="bg-white shadow">
7      <div className="px-2 mx-auto max-w-7xl sm:px-4 lg:px-8">
8        <div className="flex justify-between h-16">
9          <div className="flex px-2 lg:px-0">
10            <div className="hidden lg:flex">
11              <NavItem name="Todos" path="/dashboard" />
12              <NavItem
13                name="Create"
14                iconClass="ri-add-fill"
15                path="/tasks/create"
16              />
17            </div>
18          </div>
19          <div className="flex items-center justify-end">
20            <a
21              className="inline-flex items-center px-1 pt-1 text-sm
22             font-semibold leading-5 text-bb-gray-600 text-opacity-50
23             transition duration-150 ease-in-out border-b-2
24             border-transparent hover:text-bb-gray-600 focus:outline-none
25             focus:text-bb-gray-700 cursor-pointer"
26            >
27              LogOut
28            </a>
29          </div>
30        </div>
31      </div>
32    </nav>
33  );
34};
35
36export default NavBar;

Add remixicon to dependencies

Run the below command to add remixicon package:

1yarn add remixicon

We also need to add the remixicon CSS to our JavaScript stylesheets, in order to properly render those icons:

Append the following lines to app/javascript/stylesheets/application.scss:

1@import url("https://cdn.jsdelivr.net/npm/remixicon@2.5.0/fonts/remixicon.css");

Container component

To create a Container component, run the following command:

1touch app/javascript/src/components/Container.jsx

Now, open app/javascript/components/Container.jsx and paste the following content:

1import React from "react";
2import NavBar from "components/NavBar";
3
4import PropTypes from "prop-types";
5
6const Container = ({ children }) => {
7  return (
8    <>
9      <NavBar />
10      <div className="px-4 py-2 mx-auto max-w-7xl sm:px-6 lg:px-8">
11        <div className="max-w-3xl mx-auto">{children}</div>
12      </div>
13    </>
14  );
15};
16
17Container.propTypes = {
18  children: PropTypes.node.isRequired,
19};
20
21export default Container;

To import the Container component in any of the views, we can do the following:

1import Container from "components/Container";

Input component

To create an Input component, run the following command:

1touch app/javascript/src/components/Input.jsx

In Input.jsx, paste the following content:

1import React from "react";
2import PropTypes from "prop-types";
3
4const Input = ({
5  type = "text",
6  label,
7  value,
8  onChange,
9  placeholder,
10  required = true,
11}) => {
12  return (
13    <div className="mt-6">
14      {label && (
15        <label
16          className="block text-sm font-medium
17              leading-5 text-bb-gray-700"
18        >
19          {label}
20        </label>
21      )}
22      <div className="mt-1 rounded-md shadow-sm">
23        <input
24          type={type}
25          required={required}
26          value={value}
27          onChange={onChange}
28          placeholder={placeholder}
29          className="block w-full px-3 py-2 placeholder-gray-400
30          transition duration-150 ease-in-out border
31          border-gray-300 rounded-md appearance-none
32          focus:outline-none focus:shadow-outline-blue
33          focus:border-blue-300 sm:text-sm sm:leading-5"
34        />
35      </div>
36    </div>
37  );
38};
39
40Input.propTypes = {
41  type: PropTypes.string,
42  label: PropTypes.string,
43  value: PropTypes.node,
44  placeholder: PropTypes.string,
45  onChange: PropTypes.func,
46  required: PropTypes.bool,
47};
48
49export default Input;

We can import this Input component by using following line:

1import Input from "components/Input";

Table component

To create a Table component we will create a TableRow and TableHeader component:

1mkdir -p app/javascript/src/components/Tasks/Table/
2touch app/javascript/src/components/Tasks/Table/TableRow.jsx

In TableRow.jsx, paste the following content:

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      {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
16            className="px-6 py-4 text-sm font-medium
17            leading-5 text-bb-gray whitespace-no-wrap"
18          >
19            {rowData.user_id}
20          </td>
21          <td
22            className="px-6 py-4 text-sm font-medium
23            leading-5 text-right cursor-pointer"
24          >
25            <a
26              className="text-bb-purple text-opacity-50
27              hover:text-opacity-100"
28            >
29              Edit
30            </a>
31          </td>
32          <td
33            className="px-6 py-4 text-sm font-medium
34            leading-5 text-right cursor-pointer"
35          >
36            <a className=" hover:text-bb-red">Delete</a>
37          </td>
38        </tr>
39      ))}
40    </tbody>
41  );
42};
43
44TableRow.propTypes = {
45  data: PropTypes.array.isRequired,
46  destroyTask: PropTypes.func,
47  updateTask: PropTypes.func,
48};
49
50export default TableRow;

Create TableHeader component by running the following command:

1touch app/javascript/src/components/Tasks/Table/TableHeader.jsx

In TableHeader.jsx, parse the following content:

1import React from "react";
2import { compose, head, join, juxt, tail, toUpper } from "ramda";
3
4const TableHeader = () => {
5  return (
6    <thead>
7      <tr>
8        <th className="w-1"></th>
9        <th
10          className="px-6 py-3 text-xs font-bold leading-4 tracking-wider
11        text-left text-bb-gray-600 text-opacity-50 uppercase bg-gray-50"
12        >
13          Title
14        </th>
15        <th
16          className="px-6 py-3 text-sm font-bold leading-4 tracking-wider
17        text-left text-bb-gray-600 text-opacity-50 bg-gray-50"
18        >
19          Assigned To
20        </th>
21        <th className="px-6 py-3 bg-gray-50"></th>
22      </tr>
23    </thead>
24  );
25};
26
27export default TableHeader;

Now, create an index.jsx file inside the Table folder by running the following command:

1touch app/javascript/src/components/Tasks/Table/index.jsx

Paste the following content in Table/index.jsx:

1import React from "react";
2
3import TableHeader from "./TableHeader";
4import TableRow from "./TableRow";
5
6const Table = ({ data, destroyTask, updateTask }) => {
7  return (
8    <div className="flex flex-col mt-10 ">
9      <div className="my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
10        <div className="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
11          <div className="overflow-hidden border-b border-gray-200 shadow md:custom-box-shadow">
12            <table className="min-w-full divide-y divide-gray-200">
13              <TableHeader />
14              <TableRow data={data} />
15            </table>
16          </div>
17        </div>
18      </div>
19    </div>
20  );
21};
22
23export default Table;

Now, Table can be imported by using following line:

1import Table from "components/Tasks/Table";

PageLoader component

To create a PageLoader component, run the following command:

1touch app/javascript/src/components/PageLoader.jsx

In PageLoader.jsx, paste the following content:

1import React from "react";
2
3const PageLoader = () => {
4  return (
5    <div className="flex flex-row items-center justify-center w-screen h-screen">
6      <h1 className="text-lg leading-5">Loading...</h1>
7    </div>
8  );
9};
10
11export default PageLoader;

To use the loader component use the following line.

1import PageLoader from "components/PageLoader";

Now let's commit these changes:

1git add -A
2git commit -m "Added reusable components"

References

⌘K
    to navigateEnterto select Escto close
    Previous
    Next