Introduction
One of the main objectives when creating applications with React is to create components that are not only useful and aesthetically pleasing but also maintainable and customizable. In order to accomplish these objectives, the Interface Segregation Principle (ISP), an idea from object-oriented design, is essential. This post will explore the Interface Segregation Principle and show how it can be used to build React components that are more robust and simpler to maintain. We will examine and contrast a good and bad component design approach in order to comprehend how this idea is applied in React.
Understanding the Interface Segregation Principle
One of the SOLID principles of object-oriented design that Robert C. Martin proposed is the interface segregation principle. It focuses on the idea that no class (or component, in the context of React), should be made to rely on interfaces that it does not use. To put it another way, a component shouldn't have to deal with methods or functionality that it doesn't require.
The building blocks of the user interface in React are called components. They contain logic, rendering, and occasionally even state. In React, implementing the Interface Segregation Principle entails building components with only the methods and attributes they need, without any superfluous or pointless functionality.
Benefits of Applying ISP in React
Modularity and maintainability: React components become more modular by following ISP. Every component focuses on a certain action and serves an independent purpose. Modularity reduces the risk that changes in one component may have an impact on unrelated portions of the application, making it simpler to maintain, debug, and improve components over time.
Code reuse: ISP promotes the development of small, specific components. These elements can be reused throughout the application, resulting in a more effective use of the available code components. This reuse lessens duplication and encourages a more reliable user experience.
Reduced Coupling: In React, components can have dependencies on other components. By following ISP, components only rely on the methods and properties they need from other components, reducing coupling. This isolation minimizes the impact of changes to one component on others, leading to a more stable codebase.
Applying ISP in React Components: Good vs. Bad Approach
Good approach
In our good approach, we focus on building a Product
component that adheres to ISP. Specifically, let's examine the way we handle rendering a product's thumbnail image:
- Product component
import { Thumbnail } from "./thumbnail";
export interface IProduct {
id: string;
title: string;
price: number;
rating: { rate: number };
image: string;
}
interface IProductProps {
product: IProduct;
}
export function Product(props: IProductProps) {
const { product } = props;
const { id, title, price, rating, image } = product;
return (
<div className="w-56 flex flex-col items-center m-2 max-w-sm bg-white rounded-lg shadow-md dark:bg-gray-800 dark:border-gray-700">
<a href="#">
<Thumbnail imageUrl={product.image} />
</a>
<div className="flex flex-col px-5 pb-5">
<a href="#">
<h5 className="text-lg font-semibold tracking-tight text-gray-900 dark:text-white">
{title}
</h5>
</a>
<div className="flex items-center mt-2.5 mb-5 flex-1">
{Array(Math.trunc(rating.rate))
.fill("")
.map((_, idx) => (
<svg
aria-hidden="true"
className="w-5 h-5 text-yellow-300"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<title>First star</title>
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path>
</svg>
))}
<span className="bg-blue-100 text-blue-800 text-xs font-semibold mr-2 px-2.5 py-0.5 rounded dark:bg-blue-200 dark:text-blue-800 ml-3">
{Math.trunc(rating.rate)}
</span>
</div>
<div className="flex flex-col items-between justify-around">
<span className="text-2xl font-bold text-gray-900 dark:text-white">
${price}
</span>
<a
href="#"
className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
onClick={() => {} // do something}
>
Add to cart
</a>
</div>
</div>
</div>
);
}
Here, the Product
component only passes the necessary information—the image URL—to the Thumbnail
component. This encapsulation ensures that Product
does not expose more data than needed.
<Thumbnail imageUrl={product.image} />
- Thumbnail component
interface IThumbnailProps {
imageUrl: string;
}
export function Thumbnail(props: IThumbnailProps) {
const { imageUrl } = props;
return (
<img
className="p-8 rounded-t-lg h-48"
src={imageUrl}
alt="product image"
/>
);
}
Bad approach
In the bad approach, we disregard ISP and introduce potential issues:
- Product Component
import { useMutation, useQueryClient } from "@tanstack/react-query";
import axios from "axios";
import styled from "styled-components";
import { Thumbnail } from "./thumbnail";
export interface IProduct {
id: string;
title: string;
price: number;
rating: { rate: number };
image: string;
}
interface IProductProps {
product: IProduct;
}
export function Product(props: IProductProps) {
const { product } = props;
const { id, title, price, rating, image } = product;
return (
<div className="w-56 flex flex-col items-center m-2 max-w-sm bg-white rounded-lg shadow-md dark:bg-gray-800 dark:border-gray-700">
<a href="#">
<Thumbnail product={product} />
</a>
<div className="flex flex-col px-5 pb-5">
<a href="#">
<h5 className="text-lg font-semibold tracking-tight text-gray-900 dark:text-white">
{title}
</h5>
</a>
<div className="flex items-center mt-2.5 mb-5 flex-1">
{Array(Math.trunc(rating.rate))
.fill("")
.map((_, idx) => (
<svg
aria-hidden="true"
className="w-5 h-5 text-yellow-300"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<title>First star</title>
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path>
</svg>
))}
<span className="bg-blue-100 text-blue-800 text-xs font-semibold mr-2 px-2.5 py-0.5 rounded dark:bg-blue-200 dark:text-blue-800 ml-3">
{Math.trunc(rating.rate)}
</span>
</div>
<div className="flex flex-col items-between justify-around">
<span className="text-2xl font-bold text-gray-900 dark:text-white">
${price}
</span>
<a
href="#"
className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
onClick={() => {} // do something}
>
Add to cart
</a>
</div>
</div>
</div>
);
}
In this case, the entire product
object is passed to the Thumbnail
component, which then needs to extract the required image property. This approach results in unnecessary coupling between components and violates ISP by exposing more information than necessary.
<Thumbnail product={product} />
- Thumbnail Component
import { IProduct } from "./product";
interface IThumbnailProps {
product: IProduct;
}
export function Thumbnail(props: IThumbnailProps) {
const { product } = props;
const { image } = product;
return (
<img
className="p-8 rounded-t-lg h-48"
src={image}
alt="product image"
/>
);
}
In the bad approach demonstrated in the provided Thumbnail
component code, unnecessary work is introduced due to the violation of the Interface Segregation Principle (ISP). The Thumbnail
component receives the entire product
object as a prop, even though it only requires the image
property to display the product image. Consequently, within the Thumbnail
component, there's an additional step of extracting the image
property from the received product
object.
This extra extraction process adds complexity to the component and increases the chances of errors, as well as impeding code clarity and maintainability. Adhering to ISP by passing only the necessary data, such as the image URL, from the parent component (Product
) to the Thumbnail
component would streamline the process and eliminate this unnecessary overhead, resulting in cleaner, more focused, and efficient code.
Conclusion
In this article, we've dived into the Interface Segregation Principle and its impact on React component design. By comparing a good approach with a bad one, we've seen how adhering to ISP can lead to more modular, maintainable, and reusable code. When building your React components, always strive to encapsulate functionality appropriately, passing only the necessary data to child components. This practice aligns with the Interface Segregation Principle and contributes to a more robust and flexible codebase. As you continue your React journey, keep in mind that following ISP is not just a best practice—it's a fundamental principle that can significantly enhance the quality of your applications.