Introduction
React is one of today's most popular JavaScript frameworks, and for good reason. It offers both flexibility and ease of use. By "flexibility," I mean that React provides multiple ways to accomplish the same task. For instance, you can fetch data from a server using either the useEffect hook or a custom hook or an async Thunk using redux.
While this flexibility makes React appealing, it can also lead to challenges. Imagine a scenario where multiple developers work on the same project, each choosing a different approach to data fetching. Such inconsistencies can create problems in a React application, leading to a fragmented codebase that is difficult to maintain.
In this blog, we will explore how to address these challenges by adopting some well-known, community-approved patterns. These patterns offer tested and reliable ways to achieve specific functionalities in React. So, let's dive in!
Layout Components
layout components are the components that deal with arranging other components in the page. we achieve this by splitting the layout styles into their own component and pass the components we want to display as children to layout component. This helps by separating the component itself from where it's being displayed which makes the component more reusable.
Example: Modal layout component
This component is designed to display content prominently against a blurred background. By encapsulating this behavior within the Modal component, we make it effortlessly reusable across the entire application.
// Modal.jsx
import styled from "styled-components";
const ModalBackground = styled.div`
position: fixed;
z-index: 1;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0, 0, 0, 0.5);
`;
const ModalBody = styled.div`
background-color: white;
margin: 10% auto;
padding: 20px;
width: 50%;
`;
const Modal = ({ children, isOpen }) => {
return (
<>
{isOpen && (
<ModalBackground>
<ModalBody>{children}</ModalBody>
</ModalBackground>
)}
</>
);
};
export default Modal;
// usage in App.jsx
function App() {
const [showTermsAndConditions, setShowTermsAndConditions] = useState();
return (
<>
<button onClick={() => setShowTermsAndConditions(true)}>Terms and Conditions</button>
<Modal isOpen={showTermsAndConditions}>
<div>
<h1>Terms and Conditions</h1>
<p>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Soluta molestias maxime nesciunt neque laboriosam,
harum expedita quidem dignissimos, aliquid impedit illo voluptatum? Possimus voluptatem quia modi molestias
deleniti, voluptas facere?
</p>
<button onClick={() => setShowTermsAndConditions(false)}>Close</button>
</div>
</Modal>
</>
);
}
HOC
Higher-Order Components (HOCs) serve as a powerful and flexible pattern in React for augmenting the behavior of existing components. They are particularly invaluable in legacy codebases, offering a seamless way to extend component functionality without introducing breaking changes. By wrapping the original component within a HOC, you can inject new props, manipulate state, or even alter the rendering behavior, all while keeping the original component intact.
Example: List Component
Imagine you have a List component that displays a collection of items. Now, let's say you want to enhance this component to show a loading text or an animation while the data is being fetched from an API. This is where the Higher-Order Component (HOC) pattern shines. By wrapping your existing List component with a HOC, you can seamlessly introduce this new loading feature without altering the core functionality of the List component itself.
// List.jsx
const List = ({ items }) => {
return (
<ul>
{items.map((item, index) => (
<p key={index}>{item}</p>
))}
</ul>
);
};
export default List;
// listWithLoading.js
const withLoading = (ListComponent) => {
return ({ isLoading, ...listProps }) => {
console.log(listProps, isLoading);
if (isLoading) {
return <h2>Loading...</h2>;
}
return <ListComponent {...listProps} />;
};
};
export default withLoading;
// usage
const initialItems = ["gone girl", "fight club", "batman"];
const Index = () => {
const [items, setItems] = useState([]);
useEffect(() => {
// mimic api call
setTimeout(() => {
setItems([...initialItems]);
}, 2000);
}, []);
return (
<>
<div>
<h1>List without loading feature</h1>
<List items={items} />
</div>
<div>
<h1>List with loading feature</h1>
<ListWithLoading isLoading={items.length === 0} items={items} />
</div>
</>
);
};
export default Index;
Compound Components
Compound components are used in scenarios where you have a complex UI component that needs to share implicit state or behavior among its children, but you also want to allow for customization and flexibility in how those children are rendered. Compound components simplify the management of complex state and allows us to write readable and maintainable code.
Example: Tabs Component
This Tabs component will serve as a container for multiple Tabs, each responsible for rendering individual tab and their corresponding content. By leveraging the compound components pattern, we can centralize the management of the active tab state while offering the flexibility to customize the appearance and behavior of each tab.
// Tabs.jsx
import React, { useState } from "react";
function Tabs({ children }) {
const [activeTabIndex, setActiveTabIndex] = useState(0);
return (
<div style={{ display: "flex", width: "max-content" }}>
{React.Children.map(children, (child, index) => {
return React.cloneElement(child, { index, activeTabIndex, setActiveTabIndex });
})}
</div>
);
}
function Tab({ index, children, activeTabIndex, setActiveTabIndex }) {
return (
<div style={{ display: "flex", flexDirection: "column" }}>
{React.Children.map(children, (child) => {
return React.cloneElement(child, { index, activeTabIndex, setActiveTabIndex });
})}
</div>
);
}
function TabHeader({ children, index, activeTabIndex, setActiveTabIndex }) {
return (
<button onClick={() => setActiveTabIndex(index)} style={{ fontSize: index === activeTabIndex ? "bold" : "" }}>
{children}
</button>
);
}
function TabContent({ children, index, activeTabIndex }) {
return <>{index === activeTabIndex ? children : null}</>;
}
export { Tabs, Tab, TabHeader, TabContent };
// usage
function App() {
return (
<Tabs>
<Tab>
<TabHeader>Tab 1</TabHeader>
<TabContent>Content 1</TabContent>
</Tab>
<Tab>
<TabHeader>Tab 2</TabHeader>
<TabContent>Content 2</TabContent>
</Tab>
</Tabs>
);
}
Custom hooks
Custom hooks serve as a powerful tool for encapsulating complex state management and behavior within a React component. Once developed, these hooks can be effortlessly reused across multiple components that share the same requirements. By isolating this intricate logic into a custom hook, you not only enhance code reusability but also significantly simplify the state and behavior management in each individual component.
Example: useUser hook
This hook is designed to fetch the currently logged-in user's information while also managing two crucial states: isLoading and isSessionExpired. The isLoading state provides a way to display a loading indicator during the data-fetching process, and the isSessionExpired state helps in handling session expiration scenarios. By encapsulating these functionalities within a custom hook, we can easily reuse this logic across multiple components, thereby adhering to the principles of clean, maintainable, and DRY (Don't Repeat Yourself) code.
// useUser.js
import { useState, useEffect } from "react";
// fetch logged in user
export default function useUser() {
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [isSessionExpired, setSessionExpired] = useState(null);
useEffect(() => {
async function fetchData() {
try {
// mimic api call and response
const token = localStorage.getItem("access_token");
const response = await fetch("https://my_app/api", {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (response.ok) {
const data = await response.json();
setUser(data);
setIsLoading(false);
setSessionExpired(false);
} else if (response.status === 401) {
setSessionExpired(true);
setIsLoading(false);
} else {
throw new Error("Something went wrong");
}
} catch (error) {
console.error("Error:", error);
setIsLoading(false);
}
}
fetchData();
}, []);
return {
user,
isLoading,
isSessionExpired,
};
}
// usage
function App() {
const { user, isLoading, isSessionExpired } = useUser();
if (isLoading) {
return <h1>Loading... your profile</h1>;
}
if (!isLoading && !isSessionExpired) {
return <h1>Something went wrrong on our side, please try again after some time</h1>;
}
if (!isLoading && isSessionExpired) {
return <h1>Session expired please login</h1>;
}
return (
<div>
<h1>My profile</h1>
<h3>{user?.name}</h3>
</div>
);
}
code: https://github.com/karthik2265/react-patterns
Conclusion
n this blog post, we've explored some of the most effective design patterns in React - Layout Components, Higher-Order Components (HOCs), Compound Components, and Custom Hooks. These patterns not only help in writing clean and maintainable code but also solve specific challenges in React development, such as managing complex state and enhancing component reusability.
While these are some of my personal favorites, the landscape of design patterns in React is vast and continuously evolving. Each project may have unique requirements that could be better served by other patterns or a combination thereof. Therefore, I encourage you to not stop here. Dive deeper into the React ecosystem, experiment with these patterns, and discover which ones fit best for your specific coding challenges.
Remember, the key to becoming a proficient React developer lies in continuous learning and adaptation. So, keep exploring, keep coding, and most importantly, keep sharing your knowledge with the community.
This conclusion wraps up the blog by summarizing the key points, encouraging further exploration, and inspiring continuous learning. It also provides a sense of closure to the reader.
Thank you for reading until the end. Please consider following the writer and this publication. Visit Stackademic to find out more about how we are democratizing free programming education around the world.
Top comments (0)