Skip to main content

A Practical Guide to Creating Reusable React Components

Hanbyeol (Benny) Joo
Share

Although React is one of the most popular and most used front-end frameworks in the world, many developers still struggle when it comes to refactoring code for improved reusability. If you’ve ever found yourself repeating the same snippet of code all throughout your React app, you’ve come to the right article.

In this tutorial, you’ll be introduced to the three most common indicators that it’s time to build a reusable React component. Then we’ll go on to look at some practical demos by building a reusable layout and two exciting React hooks.

By the time you’ve finished reading, you’ll be able to figure out by yourself when it’s appropriate to create reusable React components, and how to do so.

This article assumes a basic knowledge of React and React hooks. If you want to brush up on these topics, I recommend you to check out “Getting Started with React” guide and “Intorduction to React Hooks”.

Top Three Indicators of a Reusable React Component

First let’s look at some indications of when you might want to do this.

Repetitive creation of wrappers with the same CSS style

My favorite sign of knowing when to create a reusable component is the repetitive use of the same CSS style. Now, you may think, “Wait a minute: why don’t I simply assign the same class name to elements that share the same CSS style?” You’re absolutely right. It’s not a good idea to create reusable components every time some elements in different components share the same style. In fact, it may introduce unnecessary complexity. So you have to ask yourself one more thing: are these commonly styled elements wrappers?

For example, consider the following login and signup pages:

// Login.js
import './common.css';

function Login() {
  return (
    <div className='wrapper'>
      <main>
        {...}
      </main>
      <footer className='footer'>
        {...}
      </footer>
    </div>
  );
}
// SignUp.js
import './common.css';

function Signup() {
  return (
    <div className='wrapper'>
      <main>
        {...}
      </main>
      <footer className='footer'>
        {...}
      </footer>
    </div>
  );
}

The same styles are being applied to the container (the <div> element) and the footer of each component. So in this case, you can create two reusable components — <Wrapper /> and <Footer /> — and pass them children as a prop. For example, the login component could be refactored as follows:

// Login.js
import Footer from "./Footer.js";

function Login() {
  return (
    <Wrapper main={{...}} footer={<Footer />} />
  );
} 

As a result, you no longer need to import common.css in multiple pages or create the same <div> elements to wrap everything.

Repetitive use of event listeners

To attach an event listener to an element, you can either handle it inside useEffect() like this:

// App.js
import { useEffect } from 'react';

function App() {
  const handleKeydown = () => {
    alert('key is pressed.');
  }

  useEffect(() => {
    document.addEventListener('keydown', handleKeydown);
    return () => {
      document.removeEventListener('keydown', handleKeydown);
    }
  }, []);

  return (...);
}

Or you can do it directly inside your JSX like this, as is demonstrated in the following button component:

// Button.js
function Button() {
  return (
    <button type="button" onClick={() => { alert('Hi!')}}>
      Click me!
    </button>
  );
};

When you want to add an event listener to document or window, you’d have to go with the first method. However, as you may have already realized, the first method requires more code with the use of useEffect(), addEventListener() and removeEventListener(). So in such case, creating a custom hook will allow your components to be more concise.

There are four possible scenarios for using event listeners:

  • same event listener, same event handler
  • same event listener, different event handler
  • different event listener, same event handler
  • different event listener, different event handler

In the first scenario, you can create a hook where both the event listener and the event handler are defined. Consider the following hook:

// useEventListener.js
import { useEffect } from 'react';

export default function useKeydown() {
  const handleKeydown = () => {
    alert('key is pressed.');
  }

  useEffect(() => {
    document.addEventListener('keydown', handleKeydown);
    return () => {
      document.removeEventListener('keydown', handleKeydown);
    }
  }, []);
};

You can then use this hook in any component as follows:

// App.js
import useKeydown from './useKeydown.js';

function App() {
  useKeydown();
  return (...);
};

For the other three scenarios, I recommend creating a hook that receives an event and an event handling function as props. For example, I will pass keydown and handleKeydown as props to my custom hook. Consider the following hook:

// useEventListener.js
import { useEffect } from 'react';

export default function useEventListener({ event, handler} ) {
  useEffect(() => {
    document.addEventListener(event, props.handler);
    return () => {
      document.removeEventListener(event, props.handler);
    }
  }, []);
};

You can then employ this hook in any component as follows:

// App.js
import useEventListener from './useEventListener.js';

function App() {
  const handleKeydown = () => {
    alert('key is pressed.');
  }
  useEventListener('keydown', handleKeydown);
  return (...);
};

Repetitive use of the same GraphQL script

You don’t really need to look for signs when it comes to making GraphQL code reusable. For complex applications, GraphQL scripts for a query or a mutation easily take up 30–50 lines of code because there are many attributes to request. If you’re using the same GraphQL script more than once or twice, I think it deserves its own custom hook.

Consider the following example:

import { gql, useQuery } from "@apollo/react-hooks";

const GET_POSTS = gql`
  query getPosts {
    getPosts {
    user {
      id
      name
      ...
      }
      emojis {
         id
         ...
      }
      ...
  }
`;

const { data, loading, error } = useQuery(GET_POSTS, {
  fetchPolicy: "network-only"
});

Instead of repeating this code in every page that requests posts from the back end, you should create a React hook for this particular API:

import { gql, useQuery } from "@apollo/react-hooks";

function useGetPosts() {
  const GET_POSTS = gql`{...}`;
  const { data, loading, error } = useQuery(GET_POSTS, {
    fetchPolicy: "network-only"
  });
  return [data];
}

const Test = () => {
  const [data] = useGetPosts();
  return (
    <div>{data?.map(post => <h1>{post.text}</h1>)}</div>
  );
};

Building Out Three Reusable React Components

Now that we’ve seen some common signs of when to create a new component that you can share throughout your react application, let’s put that knowledge into practice and build out three practical demos.

1. Layout component

React is normally used for building complex web apps. This means that a large number of pages need to be developed in React, and I doubt that every page of an app will have a different layout. For instance, a web app consisting of 30 pages usually uses less than five different layouts. Therefore, building a flexible, reusable layout that can be utilized in many different pages is essential. This will save you very many lines of code and consequently a tremendous amount of time.

Consider the following React functional component:

// Feed.js
import React from "react";
import style from "./Feed.module.css";

export default function Feed() {
  return (
    <div className={style.FeedContainer}>
      <header className={style.FeedHeader}>Header</header>
      <main className={style.FeedMain}>
        {
          <div className={style.ItemList}>
            {itemData.map((item, idx) => (
              <div key={idx} className={style.Item}>
                {item}
              </div>
            ))}
          </div>
        }
      </main>
      <footer className={style.FeedFooter}>Footer</footer>
    </div>
  );
}

const itemData = [1, 2, 3, 4, 5];

This is a typical web page that has a <header>, a <main> and a <footer>. If there are 30 more web pages like this, you would easily get tired of repeatedly writing HTML tags and applying the same style over and over.

Instead, you can create a layout component that receives <header>, <main> and <footer> as props, as in the following code:

// Layout.js
import React from "react";
import style from "./Layout.module.css";
import PropTypes from "prop-types";

export default function Layout({ header, main, footer }) {
  return (
    <div className={style.Container}>
      <header className={style.Header}>{header}</header>
      <main className={style.Main}>{main}</main>
      <footer className={style.Footer}>{footer}</footer>
    </div>
  );
}

Layout.propTypes = {
  main: PropTypes.element.isRequired,
  header: PropTypes.element,
  footer: PropTypes.element
};

This component doesn’t require <header> and <footer>. So, you can use this same layout for pages regardless of whether they contain a header or a footer.

Using this layout component, you can turn your feed page into a much more sophisticated block of code:

// Feed.js
import React from "react";
import Layout from "./Layout";
import style from "./Feed.module.css";

export default function Feed() {
  return (
    <Layout
      header={<div className={style.FeedHeader}>Header</div>}
      main={
        <div className={style.ItemList}>
          {itemData.map((item, idx) => (
            <div key={idx} className={style.Item}>
              {item}
            </div>
          ))}
        </div>
      }
      footer={<div className={style.FeedFooter}>Footer</div>}
    />
  );
}

const itemData = [1, 2, 3, 4, 5];

Pro tip for creating layouts with sticky elements

Many developers tend to use position: fixed or position: absolute when they want to stick a header to the top of the viewport or a footer to the bottom. However, in the case of layouts, you should try to avoid this.

Since the elements of a layout will be the parent elements of passed props, you want to keep the style of your layout elements as simple as possible — so that passed <header>, <main>, or <footer> are styled as intended. So, I recommend applying position: fixed and display: flex to the outermost element of your layout and setting overflow-y: scroll to the <main> element.

Here’s an example:

/* Layout.module.css */
.Container {
  /* Flexbox */
  display: flex;
  flex-direction: column;

  /* Width & Height */
  width: 100%;
  height: 100%;

  /* Misc */
  overflow: hidden;
  position: fixed;
}

.Main {
  /* Width & Height */
  width: 100%;
  height: 100%;

  /* Misc */
  overflow-y: scroll;
}

Now, let’s apply some styles to your feed page and see what you’ve built:

/* Feed.module.css */
.FeedHeader {
  /* Width & Height */
  height: 70px;

  /* Color & Border */
  background-color: teal;
  color: beige;
}

.FeedFooter {
  /* Width & Height */
  height: 70px;

  /* Color & Border */
  background-color: beige;
  color: teal;
}

.ItemList {
  /* Flexbox */
  display: flex;
  flex-direction: column;
}

.Item {
  /* Width & Height */
  height: 300px;

  /* Misc */
  color: teal;
}

.FeedHeader,
.FeedFooter,
.Item {
  /* Flexbox */
  display: flex;
  justify-content: center;
  align-items: center;

  /* Color & Border */
  border: 1px solid teal;

  /* Misc */
  font-size: 35px;
}

Sticky header and footer demo

And here’s the code in action.

This is what it looks like on desktop screens.

Desktop_Layout

This is what it looks like on mobile screens.

Mobile_Layout

This layout works as intended on iOS devices, too! In case you don’t know, iOS is notorious for bringing unexpected position-related problems to the development of web apps.

2. Event Listener

Often, the same event listener is used more than once throughout a web app. In such a case, it’s a great idea to create a custom React hook. Let’s learn how to do this by developing a useScrollSaver hook, which saves the scroll position of a user’s device on a page — so that the user doesn’t need to scroll all again from the top. This hook will be useful for a web page in which a large number of elements, such as posts and comments, are listed; imagine the feed pages of Facebook, Instagram and Twitter without a scroll saver.

Let’s break down the following code:

// useScrollSaver.js
import { useEffect } from "react";

export default function useScrollSaver(scrollableDiv, pageUrl) {
  /* Save the scroll position */
  const handleScroll = () => {
    sessionStorage.setItem(
      `${pageUrl}-scrollPosition`,
      scrollableDiv.current.scrollTop.toString()
    );
  };
  useEffect(() => {
    if (scrollableDiv.current) {
      const scrollableBody = scrollableDiv.current;
      scrollableBody.addEventListener("scroll", handleScroll);
      return function cleanup() {
        scrollableBody.removeEventListener("scroll", handleScroll);
      };
    }
  }, [scrollableDiv, pageUrl]);

  /* Restore the saved scroll position */
  useEffect(() => {
    if (
      scrollableDiv.current &&
      sessionStorage.getItem(`${pageUrl}-scrollPosition`)
    ) {
      const prevScrollPos = Number(
        sessionStorage.getItem(`${pageUrl}-scrollPosition`)
      );
      scrollableDiv.current.scrollTop = prevScrollPos;
    }
  }, [scrollableDiv, pageUrl]);
}

You can see that the useScrollSaver hook needs to receive two items: scrollableDiv, which must be a scrollable container just like the <main> container in your layout above, and pageUrl, which will be used as an identifier of a page so that you can store scroll positions of multiple pages.

Step 1: Save the scroll position

First of all, you need to bind a “scroll” event listener to your scrollable container:

const scrollableBody = scrollableDiv.current;
scrollableBody.addEventListener("scroll", handleScroll);
return function cleanup() {
  scrollableBody.removeEventListener("scroll", handleScroll);
};

Now, every time scrollableDiv is scrolled by a user, a function called handleScroll will be run. In this function, you should utilize either localStorage or sessionStorage to save the scroll position. The difference is that data in localStorage doesn’t expire, while data in sessionStorage is cleared when the page session ends. You can use setItem(id: string, value: string) to save data in either storage:

const handleScroll = () => {
  sessionStorage.setItem(
    `${pageUrl}-scrollPosition`,
    scrolledDiv.current.scrollTop.toString()
  );
};

Step 2: Restore the scroll position

When a user comes back to a web page, the user should be directed to his or her previous scroll position — if there is any. This position data is currently saved in sessionStorage, and you need to take it out and use it. You can use getItem(id: string) to obtain data from the storage. Then, you simply need to set scroll-top of the scrollable container to this obtained value:

const prevScrollPos = Number(
  sessionStorage.getItem(`${pageUrl}scrollPosition`)
);
scrollableDiv.current.scrollTop = prevScrollPos;

Step 3: Use useScrollSaver hook in any web page

Now that you’ve finished creating your custom hook, you can use the hook in any web page you’d like as long as you pass the two required items to the hook: scrollableDiv and pageUrl. Let’s go back to Layout.js and use your hook in there. This will allow any web page that uses this layout to enjoy your scroll saver:

// Layout.js
import React, { useRef } from "react";
import style from "./Layout.module.css";
import PropTypes from "prop-types";
import useScrollSaver from "./useScrollSaver";

export default function Layout({ header, main, footer }) {
  const scrollableDiv = useRef(null);
  useScrollSaver(scrollableDiv, window.location.pathname);
  return (
    <div className={style.Container}>
      <header className={style.Header}>{header}</header>
      <main ref={scrollableDiv} className={style.Main}>
        {main}
      </main>
      <footer className={style.Footer}>{footer}</footer>
    </div>
  );
}

Scrollsaver Demo

And here’s the code running in a Sandbox. Try scrolling the page, then using the arrow in the bottom left and corner to reload the app.

You’ll find yourself positioned at where you left off!

3. Query/Mutation (specific to GraphQL)

If you like to use GraphQL with React, as I do, you can reduce your codebase even further by creating a React hook for GraphQL queries or mutations.

Consider the following example for running a GraphQL query getPosts():

import { gql, useQuery } from "@apollo/react-hooks";

const GET_POSTS = gql`
  query getPosts {
    getPosts {
    user {
      id
      name
      ...
      }
      emojis {
         id
         ...
      }
      ...
  }
`;

const { data, loading, error } = useQuery(GET_POSTS, {
  fetchPolicy: "network-only"
});

If there are more and more attributes to request from the back end, your GraphQL script will take up more and more space. So, instead of repeating the GraphQL script and useQuery every time you need to run query getPosts(), you can create the following React hook:

// useGetPosts.js
import { gql, useQuery } from "@apollo/react-hooks";

export default function useGetPosts() {
  const GET_POSTS = gql`
  query getPosts {
    getPosts {
    user {
      id
      name
      ...
      }
      emojis {
         id
         ...
      }
      ...
  }
  `;

  const { data, loading, error } = useQuery(GET_POSTS, {
    fetchPolicy: "network-only"
  });

  return [data, loading, error];
}

Then, you can use your useGetPosts() hook as follows:

// Feed.js
import React from "react";
import Layout from "./Layout";
import style from "./Feed.module.css";
import useGetPosts from "./useGetPosts.js";

export default function Feed() {
  const [data, loading, error] = useGetPosts();
  return (
    <Layout
      header={<div className={style.FeedHeader}>Header</div>}
      main={
        <div className={style.ItemList}>
          {data?.getPosts.map((item, idx) => (
            <div key={idx} className={style.Item}>
              {item}
            </div>
          ))}
        </div>
      }
      footer={<div className={style.FeedFooter}>Footer</div>}
    />
  );
}

Conclusion

In this article, you’ve learned the three most common indicators of a reusable React component and the three most popular uses cases. Now you have the knowledge of when to create a reusable React component and how to do so easily and professionally. You’ll soon find yourself enjoying refactoring lines of code into a sophisticated reusable React component or hook. Using these refactoring techniques, our development team at Clay was able to reduce our codebase to a manageable size. I hope you can, too!