Search
⌘K
    to navigateEnterto select Escto close

    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
    • Tooltip

    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 also need a Tooltip component to view the full title of a Task if it gets truncated due to its length.

    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};

    Install Ramda

    We recommend using Ramda library over lodash for most of the JavaScript operations and helpers since it's well maintained, functional and lightweight.

    In general when checking whether a variable is null/empty etc, use the isNil/isEmpty functions from ramda. Similarly Ramda functions are highly extensile and importantly reusable.

    Install ramda first:

    1yarn add ramda

    Button component

    The components folder should be present in your application since we ran relevant commands as part of setting up Webpacker in the previous chapters.

    But do check if the components folder exists in the app/javascript/src directory. Only run the following command if the components folder doesn't already exists.

    1mkdir -p app/javascript/src/components

    Let's now create a Button component file by running the following command:

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

    In Button.jsx paste the following content:

    1import React from "react";
    2
    3import classnames from "classnames";
    4import PropTypes from "prop-types";
    5
    6const noop = () => {};
    7
    8const Button = ({ type = "button", buttonText, onClick = noop, loading }) => {
    9  const handleClick = e => {
    10    if (!loading) {
    11      onClick(e);
    12    }
    13  };
    14
    15  return (
    16    <div className="mt-6">
    17      <button
    18        type={type}
    19        onClick={handleClick}
    20        disabled={loading}
    21        className={classnames(
    22          "relative flex justify-center w-full px-4 py-2 text-sm font-medium leading-5 text-white transition duration-150 ease-in-out  border border-transparent rounded-md group hover:bg-opacity-90 focus:outline-none",
    23          {
    24            "bg-bb-purple": !loading,
    25            "bg-bb-gray-700": loading,
    26            "cursor-wait": loading
    27          }
    28        )}
    29      >
    30        {loading ? "Loading..." : buttonText}
    31      </button>
    32    </div>
    33  );
    34};
    35
    36Button.propTypes = {
    37  type: PropTypes.string,
    38  buttonText: PropTypes.string,
    39  loading: PropTypes.bool,
    40  onClick: PropTypes.func
    41};
    42export 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 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="/" />
    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

    To display icons in our application, we will install a third-party package called remixicon. This library has a lot of icons which we can use in our code. We are not required to import the icons in our components.

    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 }) => {
    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        </tr>
    16      ))}
    17    </tbody>
    18  );
    19};
    20
    21TableRow.propTypes = {
    22  data: PropTypes.array.isRequired
    23};
    24
    25export 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";
    2
    3const TableHeader = () => {
    4  return (
    5    <thead>
    6      <tr>
    7        <th className="w-1"></th>
    8        <th
    9          className="px-6 py-3 text-xs font-bold leading-4 tracking-wider
    10        text-left text-bb-gray-600 text-opacity-50 uppercase bg-gray-50"
    11        >
    12          Title
    13        </th>
    14        <th
    15          className="px-6 py-3 text-sm font-bold leading-4 tracking-wider
    16        text-left text-bb-gray-600 text-opacity-50 bg-gray-50"
    17        >
    18          Assigned To
    19        </th>
    20        <th className="px-6 py-3 bg-gray-50"></th>
    21      </tr>
    22    </thead>
    23  );
    24};
    25
    26export 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 }) => {
    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";

    Tooltip Component

    First create the Tooltip component file by running the following command:

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

    In Tooltip.jsx, paste the following content:

    1import React, { useState } from "react";
    2
    3import PropTypes from "prop-types";
    4
    5const Tooltip = ({ direction, content, delay, children }) => {
    6  const [active, setActive] = useState(false);
    7  let timeout;
    8
    9  const showTip = () => {
    10    timeout = setTimeout(() => {
    11      setActive(true);
    12    }, delay || 400);
    13  };
    14
    15  const hideTip = () => {
    16    clearInterval(timeout);
    17    setActive(false);
    18  };
    19
    20  return (
    21    <div
    22      className="tooltip-wrapper"
    23      onMouseEnter={showTip}
    24      onMouseLeave={hideTip}
    25    >
    26      {children}
    27      {active && (
    28        <div className={`tooltip-tip ${direction || "top"}`}>{content}</div>
    29      )}
    30    </div>
    31  );
    32};
    33
    34Tooltip.propTypes = {
    35  direction: PropTypes.oneOf(["top", "left", "bottom", "right"]),
    36  delay: PropTypes.number,
    37  content: PropTypes.node,
    38  children: PropTypes.node
    39};
    40
    41export default Tooltip;

    7-in-1 Sass Pattern

    Whenever we use custom styles in our projects we prefer 7-in-1 Sass pattern to maintain our stylesheets. The name comes from the fact that the structure contains 7 folders and 1 file. We store the partial styles like variables or styles for components in 7 different folders and a single file at the root level, in our case application.scss, that imports them all along with package styles to be compiled into a CSS Stylesheet.

    We can create our 7-in-1 structure by running the following commands:

    1mkdir app/javascript/stylesheets/{abstracts,base,components,layout,pages,themes,vendors}
    2find app/javascript/stylesheets/* -type d -exec touch {}/.keep \;

    These commands will create the following structure and add a .keep file to each folder. The .keep file, also called a gitkeep file, is an empty file that is created in an empty directory so that git commits the directory. By convention, empty directories are not committed to git repositories.

    Now the stylesheets folder will be having the following structure:

    1brew install tree
    2cd app/javascript/stylesheets
    3tree
    4.
    5├── abstracts
    6├── base
    7├── components
    8├── layout
    9├── pages
    10├── themes
    11├── vendors
    12└── application.scss
    

    You can read more about the 7-in-1 pattern from the official Sass guide.

    Styling Tooltip Component

    Now we need to add some styling to our Tooltip component to make sure it is positioned correctly based on the direction prop.

    Make sure your stylesheets folder has the same structure as it was mentioned in the previous section. We will be adding _variables.scss file to /abstracts folder and _tooltip.scss to the /components folder.

    You can do so by running the following commands from the root of your project:

    1touch app/javascript/stylesheets/abstracts/_variables.scss
    2touch app/javascript/stylesheets/components/_tooltip.scss

    We will define tooltip-wrapper and tooltip-tip in _tooltip.scss, while _variables.scss will contain hardcoded values for easier customization.

    Add the following lines in stylesheets/abstracts/_variables.scss:

    1//Tooltip variables for customization
    2$tooltip-text-color: #f8f9f9;
    3$tooltip-background-color: #2f3941;
    4$tooltip-margin: 10px;
    5$tooltip-arrow-size: 6px;

    Now copy and paste the following lines to stylesheets/components/_tooltip.scss:

    1//Wrapper for the element truncating
    2.tooltip-wrapper {
    3  display: flex;
    4  position: absolute;
    5}
    6
    7//Positioning tooltip along with the wrapped element
    8.tooltip-tip {
    9  position: absolute;
    10  border-radius: 4px;
    11  left: 50%;
    12  transform: translateX(-50%);
    13  padding: 6px;
    14  color: $tooltip-text-color;
    15  background: $tooltip-background-color;
    16  font-size: 14px;
    17  font-family: sans-serif;
    18  line-height: 1;
    19  z-index: 100;
    20  white-space: nowrap;
    21  //Adding the tooltip arrow
    22  &::before {
    23    content: " ";
    24    left: 50%;
    25    border: solid transparent;
    26    height: 0;
    27    width: 0;
    28    position: absolute;
    29    pointer-events: none;
    30    border-width: $tooltip-arrow-size;
    31    margin-left: calc(#{$tooltip-arrow-size} * -1);
    32  }
    33  //Style if the tooltip is to be displayed at the top of the wrapped element
    34  &.top {
    35    top: calc(#{$tooltip-margin + 15px} * -1);
    36    left: 50%;
    37    // Adjusting the tooltip arrow for direction
    38    &::before {
    39      top: 100%;
    40      border-top-color: $tooltip-background-color;
    41    }
    42  }
    43  //Style if the tooltip is to be displayed at the right of the wrapped element
    44  &.right {
    45    left: calc(100% + #{$tooltip-margin});
    46    top: 50%;
    47    transform: translateX(0) translateY(-50%);
    48    // Adjusting the tooltip arrow for direction
    49    &::before {
    50      left: calc(#{$tooltip-arrow-size} * -1);
    51      top: 50%;
    52      transform: translateX(0) translateY(-50%);
    53      border-right-color: $tooltip-background-color;
    54    }
    55  }
    56
    57  //Style if the tooltip is to be displayed at the bottom of the wrapped element
    58  &.bottom {
    59    bottom: calc(#{$tooltip-margin} * -1);
    60    // Adjusting the tooltip arrow for direction
    61    &::before {
    62      bottom: 100%;
    63      border-bottom-color: $tooltip-background-color;
    64    }
    65  }
    66  //Style if the tooltip is to be displayed at the left of the wrapped element
    67  &.left {
    68    left: auto;
    69    right: calc(100% + #{$tooltip-margin});
    70    top: 50%;
    71    transform: translateX(0) translateY(-50%);
    72    // Adjusting the tooltip arrow for direction
    73    &::before {
    74      left: auto;
    75      right: calc(#{$tooltip-arrow-size} * -2);
    76      top: 50%;
    77      transform: translateX(0) translateY(-50%);
    78      border-left-color: $tooltip-background-color;
    79    }
    80  }
    81}

    Finally, we need to import these stylesheets in stylesheets/application.scss, like so:

    1//Previous code
    2@import "abstracts/variables";
    3@import "components/tooltip";

    The Ampersand Operator in Sass

    You might have noticed the use of & inside of tooltip-tip class in our _tooltip.scss stylesheet. The & operator is an extremely useful feature in Sass. It is used in nesting when you want to create a more specific selector, for example when you want to select an element that has both the classes:

    1.tooltip-tip {
    2  &.top {
    3    //styles only for elements with class .tooltip-tip.top
    4  }
    5}

    In the above code, the block after & compiles to:

    1.tooltip-tip.top {
    2  /*styles only for elements with class .tooltip-tip.top*/
    3}

    Here are a few references to learn more about the & operator in Sass:

    The BEM Model

    One of the important use cases of the & operator is to implement the BEM model.

    BEM (Body.Element.Model) is a naming methodology which aims to solve many of the problems you'll commonly encounter when naming classes and structuring your CSS. It also enables you to create reusable front end components.

    Essentially Blocks are independent, reusable components in a webpage like tooltip-wrapper in our case. These blocks can have elements and modifiers.

    Elements are children of Blocks, and in our case, it's tooltip-tip. Elements can have modifiers applied to them.

    Modifiers represent different states or styles of classes. In our case this would be tooltip-tip.top,tooltip-tip.bottom etc.

    1//Element
    2.tooltip-tip {
    3  //Modifier
    4  &.top {
    5  }
    6}

    These modifiers can be used to control the style or state of a Component conditionally from React. For example in our Tooltip component we can specify the direction prop to specify which of the top, bottom, left or right modifier's styles get applied on our Tooltip component.

    For example We can pass bottom value to the direction prop like so:

    1<Tooltip content="Bottom Tooltip" direction="bottom">
    2  <h1>Tooltip Child</h1>
    3</Tooltip>

    This will result in only the bottom style block being applied to tooltip-tip:

    1.tooltip-tip {
    2  //Previous styles
    3  &.bottom {
    4    bottom: calc(#{$tooltip-margin} * -1);
    5    // Adjusting the tooltip arrow for direction
    6    &::before {
    7      bottom: 100%;
    8      border-bottom-color: $tooltip-background-color;
    9    }
    10  }
    11}

    You can read the more about the BEM Model in the following references:

    To import the Tooltip component refer to the following line:

    1import Tooltip from "components/Tooltip";

    And then wrap the component we want tooltip over with the <Tooltip> component.

    Now let's commit these changes:

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

    References

    Previous
    Next