Best practices for writing clean code in React - Interface Segregation Principle
What is Interface Segregation Principle ?
The Interface Segregation Principle (ISP) is a fundamental concept in object-oriented programming that states that a class should not be forced to implement interfaces it does not use. In the context of React components, adhering to ISP ensures that components have well-defined and focused interfaces that only include the methods and properties they need. This promotes code clarity, reusability, and maintainability by preventing unnecessary dependencies and reducing the risk of breaking changes.
Why is ISP important?
The Interface Segregation Principle (ISP) is important because it promotes code clarity, reusability, and maintainability by ensuring that components have well-defined and focused interfaces. By adhering to ISP in React, we can avoid unnecessary dependencies and reduce the risk of breaking changes when modifying or extending our components. This principle helps us create more modular, flexible, and scalable code that is easier to understand, test, and maintain over time.
Let’s see an examples
Overloaded Component Interface
In this example, the Dashboard component handles multiple widgets and their configurations directly, making it overly complex and hard to maintain.
// types.ts
export interface ChartWidgetProps {
title: string;
data: number[];
}
export interface TableWidgetProps {
title: string;
columns: string[];
rows: string[][];
}
export interface WeatherWidgetProps {
location: string;
temperature: number;
condition: string;
}
// Dashboard.tsx
import React from 'react';
import { ChartWidgetProps, TableWidgetProps, WeatherWidgetProps } from './types';
interface DashboardProps {
chartWidget: ChartWidgetProps;
tableWidget: TableWidgetProps;
weatherWidget: WeatherWidgetProps;
}
const Dashboard: React.FC<DashboardProps> = ({ chartWidget, tableWidget, weatherWidget }) => {
return (
<div className="dashboard">
<div className="chart-widget">
<h2>{chartWidget.title}</h2>
{/* Chart rendering logic */}
</div>
<div className="table-widget">
<h2>{tableWidget.title}</h2>
<table>
<thead>
<tr>
{tableWidget.columns.map((col, index) => (
<th key={index}>{col}</th>
))}
</tr>
</thead>
<tbody>
{tableWidget.rows.map((row, rowIndex) => (
<tr key={rowIndex}>
{row.map((cell, cellIndex) => (
<td key={cellIndex}>{cell}</td>
))}
</tr>
))}
</tbody>
</table>
</div>
<div className="weather-widget">
<h2>Weather in {weatherWidget.location}</h2>
<p>{weatherWidget.temperature}°C and {weatherWidget.condition}</p>
</div>
</div>
);
};
export default Dashboard;
Following Interface Segregation Principle
In this improved example, we break down the Dashboard component into smaller, focused components for each widget type. Each component has a specific responsibility, making the code more modular and maintainable.
// types.ts
export interface ChartWidgetProps {
title: string;
data: number[];
}
export interface TableWidgetProps {
title: string;
columns: string[];
rows: string[][];
}
export interface WeatherWidgetProps {
location: string;
temperature: number;
condition: string;
}
// ChartWidget.tsx
import React from 'react';
import { ChartWidgetProps } from './types';
const ChartWidget: React.FC<ChartWidgetProps> = ({ title, data }) => {
return (
<div className="chart-widget">
<h2>{title}</h2>
{/* Chart rendering logic */}
</div>
);
};
export default ChartWidget;
// TableWidget.tsx
import React from 'react';
import { TableWidgetProps } from './types';
const TableWidget: React.FC<TableWidgetProps> = ({ title, columns, rows }) => {
return (
<div className="table-widget">
<h2>{title}</h2>
<table>
<thead>
<tr>
{columns.map((col, index) => (
<th key={index}>{col}</th>
))}
</tr>
</thead>
<tbody>
{rows.map((row, rowIndex) => (
<tr key={rowIndex}>
{row.map((cell, cellIndex) => (
<td key={cellIndex}>{cell}</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
};
export default TableWidget;
// WeatherWidget.tsx
import React from 'react';
import { WeatherWidgetProps } from './types';
const WeatherWidget: React.FC<WeatherWidgetProps> = ({ location, temperature, condition }) => {
return (
<div className="weather-widget">
<h2>Weather in {location}</h2>
<p>{temperature}°C and {condition}</p>
</div>
);
};
export default WeatherWidget;
// Dashboard.tsx
import React from 'react';
import { ChartWidgetProps, TableWidgetProps, WeatherWidgetProps } from './types';
import ChartWidget from './ChartWidget';
import TableWidget from './TableWidget';
import WeatherWidget from './WeatherWidget';
interface DashboardProps {
chartWidget: ChartWidgetProps;
tableWidget: TableWidgetProps;
weatherWidget: WeatherWidgetProps;
}
const Dashboard: React.FC<DashboardProps> = ({ chartWidget, tableWidget, weatherWidget }) => {
return (
<div className="dashboard">
<ChartWidget {...chartWidget} />
<TableWidget {...tableWidget} />
<WeatherWidget {...weatherWidget} />
</div>
);
};
export default Dashboard;
Explanation
- Bad example:
- The Dashboard component directly handles rendering logic for all widget types, making it monolithic and harder to maintain.
- The interface is overloaded as it includes properties for all types of widgets, violating ISP.
- Good example:
- We created separate components for each widget type (ChartWidget, TableWidget, WeatherWidget).
- Each component has a focused interface that includes only the properties it needs, adhering to ISP.
- The Dashboard component is simplified, composing these smaller components without handling their internal details.
This approach adheres to the Interface Segregation Principle, making the codebase more modular, easier to maintain, and scalable.
Summary
By refactoring the widget components to follow the Interface Segregation Principle (ISP), we ensure that each widget handles its own specific responsibilities, reducing unnecessary complexity. This approach promotes modularity, maintainability, and clarity, ultimately leading to a more robust and scalable React application. Each component becomes easier to understand, test, and extend, contributing to a cleaner and more efficient codebase.