Achieving Reusability With React Composition

January 13, 2021
Christian IpanaqueChristian Ipanaque

In this article, you'll learn how to use React Composition to create reusable and extendable components. This will help you identify opportunities to remove duplicate code and make your application easier to maintain as you hire new engineers for your team. When new features are constantly added without having to modify multiple files you'll, instead, modify a single source.

Photo by Nick Fewings

What Is React Composition?

React Composition is a development pattern based on React's original component model where we build components from other components using explicit defined props or the implicit children prop.

In terms of refactoring, React composition is a pattern that can be used to break a complex component down to smaller components, and then composing those smaller components to structure and complete your application.

Why Use React Composition?

This technique prevents us from building too many similar components containing duplicate code and allows us to build fewer components that can be reused anywhere within our application, making them easier to understand and maintain for your team.

First, let's start examining the components that we'll be refactoring:

Accordion Component

import React, { useState } from "react"; const Accordion = () => { const [expanded, setExpanded] = useState(false); const toggleExpanded = () => { setExpanded((prevExpanded) => !prevExpanded); }; return ( <div> <button onClick={toggleExpanded}> Header <span>{expanded ? "-" : "+"}</span> </button> {expanded && <div>Content</div>} </div> ); }; export default Accordion;
import React from "react"; import Accordion from "./components/Accordion"; const App = () => { return <Accordion />; }; export default App;

Editable Component

import React, { useState } from "react"; const Editable = () => { const [editable, setEditable] = useState(false); const [inputValue, setInputValue] = useState("Title"); const toggleEditable = () => { setEditable((prevEditable) => !prevEditable); }; const handleInputChange = (e) => { setInputValue(e.target.value); }; return ( <div> {editable ? ( <label htmlFor="title"> Title: <input type="text" id="title" value={inputValue} onChange={handleInputChange} /> </label> ) : ( <>Title: {inputValue}</> )} <button onClick={toggleEditable}>{editable ? "Cancel" : "Edit"}</button> </div> ); }; export default Editable;
import React from "react"; import Editable from "./components/Editable"; const App = () => { return <Editable />; }; export default App;

Let's explore one similarity between these two components.

Problem: Notice how both the Accordion component and the Editable component share the same functionality, where both are dependent on a boolean and a function to update that boolean — in other words, a toggle functionality.

Solution: We can use a custom hook that will allow us to reuse this toggle logic in both components, and in any new component added in the future.

Introducing Custom Hooks

The purpose of creating a custom hook is to extract logic from a component and convert it into a reusable hook.

A reusable custom hook is used to avoid creating too many similar components that share the same logic. It also improves the code of your application by removing duplicate code, making your application easier to maintain. When introducing a new feature in your application, creating a custom hook prevents us from implementing that same new feature to each similarly built component. Instead, we can now reuse a custom hook.

Let's create a custom hook named useToggle that returns a status state and a toggleStatus handler function:

import { useState, useCallback, useMemo } from "react"; const useToggle = () => { const [status, setStatus] = useState(false); const toggleStatus = useCallback(() => { setStatus((prevStatus) => !prevStatus); }, []); const values = useMemo( () => ({ status, toggleStatus }), [status, toggleStatus] ); return values; }; export default useToggle;

The toggle logic implemented in useToggle is useful in the following scenarios:

  • Hiding/Displaying a component
  • Collapsing/Expanding a component

We can now reuse our new custom hook as many times as needed in any component that will take advantage of using this shared logic.

Refactoring Accordion Component To Use Custom Hook

Let's refactor the Accordion component to use the useToggle custom hook:

import React from "react"; import useToggle from "./useToggle"; const Accordion = () => { const { status: expanded, toggleStatus: toggleExpanded } = useToggle(); return ( <div> <button onClick={toggleExpanded}> Header <span>{expanded ? "-" : "+"}</span> </button> {expanded && <div>Content</div>} </div> ); }; export default Accordion;

Refactoring Editable Component To Use Custom Hook

We can also refactor the Editable component using the same useToggle custom hook:

import React, { useState } from "react"; import useToggle from "./useToggle"; const Editable = () => { const { status: editable, toggleStatus: toggleEditable } = useToggle(); const [inputValue, setInputValue] = useState("Title"); const handleInputChange = (e) => { setInputValue(e.target.value); }; return ( <div> {editable ? ( <label htmlFor="title"> Title: <input type="text" id="title" value={inputValue} onChange={handleInputChange} /> </label> ) : ( <>Title: {inputValue}</> )} <button onClick={toggleEditable}>{editable ? "Cancel" : "Edit"}</button> </div> ); }; export default Editable;

Refactoring Accordion Component To Use Specialized and Container Components

Let's examine how the header and content are displayed in our new Accordion to investigate another problem.

Problem: If we wanted to create different variations of the Accordion, e.g. CarDetailsAccordion, CommentsAccordion, PaymentOptionsAccordion, then we would have to write the same button element and expanded && content conditional in each component.

Solution: To avoid having duplicate boiler plate code when displaying the header and content of a variation of an Accordion, let's create specialized and container components.

Specialized Components

A specialized component is a component that is built from its accepted props to handle one specific case.

Let's create two new specialized components to handle displaying the header and the content of the Accordion component:

import React from "react"; const AccordionHeader = ({ children, expanded, toggleExpanded }) => { return ( <button onClick={toggleExpanded}> {children} <span>{expanded ? "-" : "+"}</span> </button> ); }; export default AccordionHeader;
import React from "react"; const AccordionContent = ({ children, expanded }) => { return <>{expanded && children}</>; }; export default AccordionContent;

Container Components

A container component, also known as a parent component, is a component that provides the state and behavior to its children components.

We can now refactor the Accordion component into a container component, and also include our two new specialized components AccordionHeader and AccordionContent:

import React from "react"; import useToggle from "./useToggle"; import AccordionHeader from "./AccordionHeader"; import AccordionContent from "./AccordionContent"; const Accordion = ({ children, header }) => { const { status: expanded, toggleStatus: toggleExpanded } = useToggle(); return ( <div> <AccordionHeader expanded={expanded} toggleExpanded={toggleExpanded}> {header} </AccordionHeader> <AccordionContent expanded={expanded}>{children}</AccordionContent> </div> ); }; export default Accordion;

Notice how we are reusing the AccordionHeader and AccordionContent components, and this can also be applied to any new component that needs them.

This is how we will use our refactored Accordion component in our application:

import React from "react"; import Accordion from "./components/Accordion"; const App = () => { return ( <> <Accordion header="Accordion 1"> <div>Content for Accordion 1</div> </Accordion> <Accordion header="Accordion 2"> <div>Content for Accordion 2</div> </Accordion> <Accordion header="Accordion 3"> <div>Content for Accordion 3</div> </Accordion> </> ); }; export default App;

Let's examine our Accordion component once again.

Problem: Notice how we are passing the expanded={expanded} prop to AccordionHeader and to AccordionContent. If we were to introduce a new specialized component that also needed the expanded={expanded} prop to be passed, we would also create additional duplicate code.

Solution: We can avoid repeating ourselves by using React's Context API, where we don't have to write and pass the same prop to all of the Accordion's children components, deeply nested components, or newly added specialized components.

Refactoring The Accordion Component To Use React's Context API

We'll use React's Context API to provide the expanded state and the toggleExpanded handler function to the entire Accordion component tree, making them available to all its children components and newly added children components. This also prevents us from having to manually pass down props when a new prop is introduced:

import React, { createContext } from "react"; import useToggle from "./useToggle"; import AccordionHeader from "./AccordionHeader"; import AccordionContent from "./AccordionContent"; export const AccordionContext = createContext(); const { Provider } = AccordionContext; const Accordion = ({ children, header }) => { const { status: expanded, toggleStatus: toggleExpanded } = useToggle(); const value = { expanded, toggleExpanded }; return ( <Provider value={value}> <div> <AccordionHeader>{header}</AccordionHeader> <AccordionContent>{children}</AccordionContent> </div> </Provider> ); }; export default Accordion;
import React, { useContext } from "react"; import { AccordionContext } from "./Accordion"; const AccordionHeader = ({ children }) => { const { expanded, toggleExpanded } = useContext(AccordionContext); return ( <button onClick={toggleExpanded}> {children} <span>{expanded ? "-" : "+"}</span> </button> ); }; export default AccordionHeader;
import React, { useContext } from "react"; import { AccordionContext } from "./Accordion"; const AccordionContent = ({ children }) => { const { expanded } = useContext(AccordionContext); return <>{expanded && children}</>; }; export default AccordionContent;

Our newly refactored Accordion component does not affect how we reuse it within our application:

import React from "react"; import Accordion from "./components/Accordion"; const App = () => { return ( <> <Accordion header="Accordion 1"> <div>Content for Accordion 1</div> </Accordion> <Accordion header="Accordion 2"> <div>Content for Accordion 2</div> </Accordion> <Accordion header="Accordion 3"> <div>Content for Accordion 3</div> </Accordion> </> ); }; export default App;

Extending The Accordion Component

Let's say our client would like us to update the - and + text that is toggled in our AccordionHeader. The client would like us to use icons instead. This can now be easily accomplished by introducing a new specialized AccordionIcon component:

import React, { useContext } from "react"; import { AccordionContext } from "./Accordion"; const AccordionIcon = ({ opened = "-", closed = "+" }) => { const { expanded } = useContext(AccordionContext); return <span>{expanded ? opened : closed}</span>; }; export default AccordionIcon;

And implement it in our AccordionHeader component:

import React, { useContext } from "react"; import { AccordionContext } from "./Accordion"; import AccordionIcon from "./AccordionIcon"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faAngleDown, faAngleUp } from "@fortawesome/free-solid-svg-icons"; const AccordionHeader = ({ children }) => { const { toggleExpanded } = useContext(AccordionContext); return ( <button onClick={toggleExpanded}> {children} <AccordionIcon opened={<FontAwesomeIcon icon={faAngleUp} />} closed={<FontAwesomeIcon icon={faAngleDown} />} /> </button> ); }; export default AccordionHeader;

Conclusion

By prioritizing reusability from the beginning of our application, we can now easily extend the Accordion component with new features that our client would like us to implement, and increase our team's speed to deliver those new features. Thus, with the help of React composition, we can provide our teams with more reusable, readable, and extendable components.

Achieving reusability is no easy task, but as we continue to grow and learn with each and every application we build for our clients, we build the experience that is needed to succeed in our careers as software engineers.

Here at Formidable, one of our core values is to provide the highest standards of quality in every line of code we write for our clients. If you would like to hire one of our skilled engineers or designers for your team, please contact us.

Related Posts

Ranked Choice Voting: The Mobile Challenge

November 19, 2024
While working on VoteHub, a mobile absentee ballot solution for U.S. elections, I was tasked with designing and prototyping an interface for a relatively new election contest type, rapidly gaining attention and adoption, called Ranked Choice Voting (RCV).

Empowering Users: Developing Accessible Mobile Apps using React Native

July 2, 2024
Robust technical accessibility strategies and best practices we implemented for a mobile voting application using React Native.

Seamless Transitions: From Native to React Native

June 4, 2024
React Native, developed by Meta, allows developers to use a single codebase to create apps that run on both iOS and Android