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
Table of Contents
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];
Continue reading A Practical Guide to Creating Reusable React Components on SitePoint.