DEV Community

Cover image for Higher Order Components: Cleaning Up Repetitive React Code
Janelle Phalon
Janelle Phalon

Posted on • Edited on

Higher Order Components: Cleaning Up Repetitive React Code

Introduction to the problem: Repetitive React components

While working on building a secure message center, I ran into a problem: my React components were repeating themselves. I was copying and pasting a lot, leading to a bunch of similar components. Especially when fetching and rendering messages through DataMotion's Secure Message Center APIs, I noticed that I was doing the same thing over and over.

Looking for a better way, I chatted with my digital buddy, Chat GPT. The advice? Use Higher Order Components (HOCs).

What are HOCs and How do they Keep Your Code DRY?

Before we dive into the solution, let's talk about why it's essential to avoid repeating code. Code that doesn't repeat itself is easier to maintain, has fewer errors, and is more readable. If you're copying and pasting a lot, there's probably a more efficient way to do things. In React, Higher Order Components can help. They let you wrap and reuse component logic, making your codebase neater and more modular.

Introducing Higher Order Components (HOCs) as a Solution

By using HOCs, I managed to simplify my codebase significantly. I went from having a folder with 5 files down to just 1:

The before -

 └── message-lists / 
│            ├── DraftList.js
│            ├── InboxList.js
│            ├── SentList.js
│            ├── TrackSent.js
│            └── TrashList.js
Enter fullscreen mode Exit fullscreen mode

The after -

  └── message-lists / 
│            └── MessageListHOC.js
Enter fullscreen mode Exit fullscreen mode

A Step-By-Step guide to Refactoring my Secure Message Center

At first, I was creating the MessageList components by copying, pasting, and adjusting the endpoints I was requesting from my backend server. Here's a look at two of these repetitive files:

DraftList.js:

import React, { useEffect, useState } from 'react';
import { ListGroup } from 'react-bootstrap';

// Inbox Components
import InboxHeader from '../components/InboxHeader';
import OpenModal from '../components/OpenModal';

const DraftList = () => {
    const [emails, setEmails] = useState([]);
    const [selectedEmail, setSelectedEmail] = useState(null);
    const [showModal, setShowModal] = useState(false);

    useEffect(() => {
      // Call our server to get emails
      fetch('http://localhost:5000/api/messages/draft')
          .then(response => {
              if (!response.ok) {
                  throw new Error('Network response was not ok');
              }
              return response.json();
          })
          .then(data => {
              // Sort emails by createTime from newest to oldest
              const sortedEmails = data.items.sort((a, b) => new Date(b.createTime) - new Date(a.createTime));
              setEmails(sortedEmails);
          })
          .catch(error => {
              console.error('Error fetching messages:', error);
          });
  }, []);

    // Format date to display in the list
    const formatDate = (dateString) => {
      const date = new Date(dateString);
      const today = new Date();
      if (date.toDateString() === today.toDateString()) {
          return date.toLocaleTimeString();
      }
      return date.toLocaleDateString();
    };

    // Sets the selectedEmails state
    const openMessage = (messageId) => {
        const email = emails.find(e => e.messageId === messageId);
        setSelectedEmail(email);
        setShowModal(true);
    };

    const deleteMessage = (messageId) => {
        setEmails(emails.filter(e => e.messageId !== messageId));
    };

    return (
      <>
      <div className='pt-3 pl-5'>
        {/* Inbox Headers */}
        <InboxHeader />
      </div>

        <ListGroup className='inbox-style'>
            {emails.map(email => (
                <div key={email.messageId}
                  className="d-flex text-secondary py-1 border-bottom border-gray email-tem"
                  onClick={() => openMessage(email.messageId)}
                >
                  <div className="col-4 email-item sender-email">{email.senderAddress}</div>
                  <div className="col-4 email-item email-subject">{email.subject}</div>
                  <div className="col-3 email-item date-received">{formatDate(email.createTime)}</div>
                  <div className="col-1 text-right pr-2 email-item">
                    {/* Delete Button */}
                    <button
                      type="button"
                      className="btn btn-outline-secondary btn-sm"
                      onClick={(e) => {
                        e.stopPropagation();
                        deleteMessage(email.messageId);
                      }}
                    >
                    </button>
                  </div>
                </div>
            ))}
        </ListGroup>

      {/* Email Modal */}
      <OpenModal email={selectedEmail} show={showModal} onClose={() => setShowModal(false)} />
      </>
    );
};

export default DraftList;
Enter fullscreen mode Exit fullscreen mode

InboxList.js:

import React, { useEffect, useState } from 'react';
import { ListGroup } from 'react-bootstrap';

// Inbox Components
import InboxHeader from '../components/InboxHeader';
import OpenModal from '../components/OpenModal';

const InboxList = () => {
    const [emails, setEmails] = useState([]);
    const [selectedEmail, setSelectedEmail] = useState(null);
    const [showModal, setShowModal] = useState(false);

    useEffect(() => {
      // Call our server to get emails
      fetch('http://localhost:5000/api/messages/inbox')
          .then(response => {
              if (!response.ok) {
                  throw new Error('Network response was not ok');
              }
              return response.json();
          })
          .then(data => {
              // Sort emails by createTime from newest to oldest
              const sortedEmails = data.items.sort((a, b) => new Date(b.createTime) - new Date(a.createTime));
              setEmails(sortedEmails);
          })
          .catch(error => {
              console.error('Error fetching messages:', error);
          });
  }, []);

    // Format date to display in the list
    const formatDate = (dateString) => {
      const date = new Date(dateString);
      const today = new Date();
      if (date.toDateString() === today.toDateString()) {
          return date.toLocaleTimeString();
      }
      return date.toLocaleDateString();
    };

    // Sets the selectedEmails state
    const openMessage = (messageId) => {
        const email = emails.find(e => e.messageId === messageId);
        setSelectedEmail(email);
        setShowModal(true);
    };

    const deleteMessage = (messageId) => {
        setEmails(emails.filter(e => e.messageId !== messageId));
    };

    return (
      <>
      <div className='pt-3 pl-5'>
        {/* Inbox Headers */}
        <InboxHeader />
      </div>

        <ListGroup className='inbox-style'>
            {emails.map(email => (
                <div key={email.messageId}
                  className="d-flex text-secondary py-1 border-bottom border-gray email-tem"
                  onClick={() => openMessage(email.messageId)}
                >
                  <div className="col-4 email-item sender-email">{email.senderAddress}</div>
                  <div className="col-4 email-item email-subject">{email.subject}</div>
                  <div className="col-3 email-item date-received">{formatDate(email.createTime)}</div>
                  <div className="col-1 text-right pr-2 email-item">
                    {/* Delete Button */}
                    <button
                      type="button"
                      className="btn btn-outline-secondary btn-sm"
                      onClick={(e) => {
                        e.stopPropagation();
                        deleteMessage(email.messageId);
                      }}
                    >
                    </button>
                  </div>
                </div>
            ))}
        </ListGroup>

      {/* Email Modal */}
      <OpenModal email={selectedEmail} show={showModal} onClose={() => setShowModal(false)} />
      </>
    );
};

export default InboxList;
Enter fullscreen mode Exit fullscreen mode

It's clear they're almost the same.

But with HOCs, I was able to put most of the repetitive code into one HOC file and then request data from my server backend through App.js.

MessageListHOC.js:

import React, { useEffect, useState } from 'react';
import { ListGroup } from 'react-bootstrap';
import InboxHeader from '../components/InboxHeader';
import OpenModal from '../components/OpenModal';

const MessageListHOC = ({ apiEndpoint }) => {
    const [emails, setEmails] = useState([]);
    const [selectedEmail, setSelectedEmail] = useState(null);
    const [showModal, setShowModal] = useState(false);

    useEffect(() => {
      fetch(apiEndpoint)
          .then(response => {
              if (!response.ok) {
                  throw new Error('Network response was not ok');
              }
              return response.json();
          })
          .then(data => {
              const sortedEmails = data.items.sort((a, b) => new Date(b.createTime) - new Date(a.createTime));
              setEmails(sortedEmails);
          })
          .catch(error => {
              console.error('Error fetching messages:', error);
          });
    }, [apiEndpoint]);


    // Format date to display in the list
    const formatDate = (dateString) => {
      const date = new Date(dateString);
      const today = new Date();
      if (date.toDateString() === today.toDateString()) {
          return date.toLocaleTimeString();
      }
      return date.toLocaleDateString();
    };

    // Sets the selectedEmails state
    const openMessage = (messageId) => {
        const email = emails.find(e => e.messageId === messageId);
        setSelectedEmail(email);
        setShowModal(true);
    };

    const deleteMessage = (messageId) => {
        setEmails(emails.filter(e => e.messageId !== messageId));
    };

    return (
      <>
      <div className='pt-3 pl-5'>
        {/* Inbox Headers */}
        <InboxHeader />
      </div>

        <ListGroup className='inbox-style'>
            {emails.map(email => (
                <div key={email.messageId}
                  className="d-flex text-secondary py-1 border-bottom border-gray email-tem"
                  onClick={() => openMessage(email.messageId)}
                >
                  <div className="col-4 email-item sender-email">{email.senderAddress}</div>
                  <div className="col-4 email-item email-subject">{email.subject}</div>
                  <div className="col-3 email-item date-received">{formatDate(email.createTime)}</div>
                  <div className="col-1 text-right pr-2 email-item">
                    {/* Delete Button */}
                    <button
                      type="button"
                      className="btn btn-outline-secondary btn-sm"
                      onClick={(e) => {
                        e.stopPropagation();
                        deleteMessage(email.messageId);
                      }}
                    >
                    </button>
                  </div>
                </div>
            ))}
        </ListGroup>

      {/* Email Modal */}
      <OpenModal email={selectedEmail} show={showModal} onClose={() => setShowModal(false)} />
      </>
    );
};

export default MessageListHOC;
Enter fullscreen mode Exit fullscreen mode

App.js:

  return (
    <div>
      <Navbar />
      <Container fluid>
        <Row className="container-box">
          <Col md={3} className="sidebar-scroll">
            <Sidebar setActiveTab={setActiveTab} />
          </Col>
          <Col md={9} className="inbox-scroll">
            {activeTab === 'inbox' && <MessageListHOC apiEndpoint="http://localhost:5000/api/messages/inbox"  />}
            {activeTab === 'drafts' && <MessageListHOC apiEndpoint="http://localhost:5000/api/messages/draft"  />}
            {activeTab === 'track-sent' && <MessageListHOC apiEndpoint="http://localhost:5000/api/messages/sent"  />}
            {activeTab === 'trash' && <MessageListHOC apiEndpoint="http://localhost:5000/api/messages/trash"  />}
          </Col>
        </Row>
      </Container>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

How This Improved From the Original

  1. Less Repetition: I went from multiple files with nearly identical code to a single HOC that handles the logic for all message lists.
  2. Easier Updates: If I need to change how the message list works in the future, I only have to update one file.
  3. Clearer Structure: With this new setup, it's much easier for anyone (including me in the future) to understand how the code works.

How to Know When You Should Implement HOCs

Sometimes, it's not immediately clear when to use HOCs. Here are some common signs that they might be helpful:

  • Repetitive Logic: If you're copying and pasting logic across multiple components, it's a sign that this logic can be abstracted into an HOC.
  • Shared State or Methods: When different components share state or methods, an HOC can help manage this shared functionality.
  • Enhancing Components: If you're frequently adding enhancements or additional props to components, an HOC can centralize these enhancements.
  • Context Sharing: When multiple components need to share context, like user authentication, an HOC can provide this context to all of them.

Conclusion: The Benefits of Modular and Efficient Code

This experience taught me a lot. Using HOCs didn't just make my codebase smaller; it made it smarter. By focusing on modular and efficient code, we can build software that's not just functional but also clean and easy to understand. Cleaner code means better software and a happier time coding.

Keep an eye out for more updates as I continue to develop my message center. Your feedback is always appreciated!

Top comments (0)