Welcome back to our SOLID principles in the React series! So far, we’ve covered:
✅ S: Single Responsibility Principle (SRP): Keep components focused.
✅ O: Open/Closed Principle (OCP): Make extensions easy, not modifications.
✅ L: Liskov Substitution Principle (LSP): Keep components interchangeable without breaking behavior.
Today, we’re diving into the Interface Segregation Principle (ISP)—a principle that helps keep our React components and interfaces lean, modular, and easier to maintain.
If SOLID principles were a gym for your code, ISP would be the exercise that trims down bloated, unfocused interfaces. Let’s break it down and then apply it to real-world React examples.
What is the Interface Segregation Principle (ISP)?
ISP is one of the five SOLID principles from object-oriented programming (OOP), but it’s just as relevant in React.
In simple terms:
“A class (or component) should not be forced to depend on interfaces it does not use.”
In React terms:
A component should only receive the props it actually needs.
Interfaces (types) should be small and specific, not overloaded with unrelated properties.
Hooks should return only the data relevant to their purpose.
If an interface has too many properties, components will be bloated, harder to reuse, and difficult to maintain.
In simple terms, ISP states that an interface should only have the methods and properties relevant to a specific use case. In React, this means breaking down component props and interfaces into smaller, focused ones. This improves maintainability, reusability, and readability.
Why Should React Developers Care?
Before we dive into code, let's talk about why this matters in real-world React development:
Easier Testing: When components have fewer responsibilities, they're much easier to test. You don't have to mock a bunch of unused props just to test one feature.
Better Reusability: Smaller, focused components are like LEGO blocks - you can mix and match them in different ways to build different features.
Simpler Maintenance: When something breaks, you know exactly where to look. Plus, changing one component is less likely to cause unexpected issues elsewhere.
Improved Performance: React can better optimize renders when components only depend on the props they actually use.
Why Does ISP Matter in React?
Violating ISP often leads to these common issues:
Bloated Components: Components receive too many props, making them complex and hard to manage.
Unnecessary Re-renders: A component re-renders even when only an unrelated prop changes.
Poor Reusability: A component tied to a massive interface becomes difficult to reuse elsewhere.
The Problem: A Monolithic Component
Let's look at a common mistake: creating a component that tries to do too much.
Here's an example of a user profile card that violates ISP:
interface UserProfileProps {
// Basic user info
userId: string;
name: string;
email: string;
avatar: string;
// Social media links
twitter?: string;
linkedin?: string;
github?: string;
// Activity stats
postCount: number;
commentCount: number;
likeCount: number;
// Admin features
isAdmin: boolean;
canDelete: boolean;
canEdit: boolean;
// Event handlers
onEdit: () => void;
onDelete: () => void;
onFollow: () => void;
onMessage: () => void;
}
const UserProfile: React.FC<UserProfileProps> = ({
userId,
name,
email,
avatar,
twitter,
linkedin,
github,
postCount,
commentCount,
likeCount,
isAdmin,
canDelete,
canEdit,
onEdit,
onDelete,
onFollow,
onMessage
}) => {
return (
<div className="user-profile">
<div className="user-basic-info">
<img src={avatar} alt={name} />
<h2>{name}</h2>
<p>{email}</p>
</div>
<div className="social-links">
{twitter && <a href={twitter}>Twitter</a>}
{linkedin && <a href={linkedin}>LinkedIn</a>}
{github && <a href={github}>GitHub</a>}
</div>
<div className="activity-stats">
<span>Posts: {postCount}</span>
<span>Comments: {commentCount}</span>
<span>Likes: {likeCount}</span>
</div>
<div className="actions">
<button onClick={onFollow}>Follow</button>
<button onClick={onMessage}>Message</button>
{isAdmin && canEdit && <button onClick={onEdit}>Edit</button>}
{isAdmin && canDelete && <button onClick={onDelete}>Delete</button>}
</div>
</div>
);
};
See the problem? This component is doing way too much:
Handling basic user information
Managing social media links
Displaying activity statistics
Dealing with admin functionality
Managing various user interactions
Applying ISP: Breaking It Down
Let's refactor this component using ISP. We'll break it into smaller, focused interfaces and components:
// Basic user information interface
interface UserBasicInfoProps {
name: string;
email: string;
avatar: string;
}
const UserBasicInfo: React.FC<UserBasicInfoProps> = ({ name, email, avatar }) => (
<div className="user-basic-info">
<img src={avatar} alt={name} />
<h2>{name}</h2>
<p>{email}</p>
</div>
);
// Social links interface
interface SocialLinksProps {
twitter?: string;
linkedin?: string;
github?: string;
}
const SocialLinks: React.FC<SocialLinksProps> = ({ twitter, linkedin, github }) => (
<div className="social-links">
{twitter && <a href={twitter}>Twitter</a>}
{linkedin && <a href={linkedin}>LinkedIn</a>}
{github && <a href={github}>GitHub</a>}
</div>
);
// Activity stats interface
interface ActivityStatsProps {
postCount: number;
commentCount: number;
likeCount: number;
}
const ActivityStats: React.FC<ActivityStatsProps> = ({
postCount,
commentCount,
likeCount
}) => (
<div className="activity-stats">
<span>Posts: {postCount}</span>
<span>Comments: {commentCount}</span>
<span>Likes: {likeCount}</span>
</div>
);
// Admin actions interface
interface AdminActionsProps {
isAdmin: boolean;
canEdit: boolean;
canDelete: boolean;
onEdit: () => void;
onDelete: () => void;
}
const AdminActions: React.FC<AdminActionsProps> = ({
isAdmin,
canEdit,
canDelete,
onEdit,
onDelete
}) => {
if (!isAdmin) return null;
return (
<div className="admin-actions">
{canEdit && <button onClick={onEdit}>Edit</button>}
{canDelete && <button onClick={onDelete}>Delete</button>}
</div>
);
};
// User interaction interface
interface UserInteractionProps {
onFollow: () => void;
onMessage: () => void;
}
const UserInteractions: React.FC<UserInteractionProps> = ({
onFollow,
onMessage
}) => (
<div className="user-interactions">
<button onClick={onFollow}>Follow</button>
<button onClick={onMessage}>Message</button>
</div>
);
// Main UserProfile component now composing smaller components
interface UserProfileProps {
user: UserBasicInfoProps;
social: SocialLinksProps;
stats: ActivityStatsProps;
admin: AdminActionsProps;
interactions: UserInteractionProps;
}
const UserProfile: React.FC<UserProfileProps> = ({
user,
social,
stats,
admin,
interactions
}) => {
return (
<div className="user-profile">
<UserBasicInfo {...user} />
<SocialLinks {...social} />
<ActivityStats {...stats} />
<AdminActions {...admin} />
<UserInteractions {...interactions} />
</div>
);
};
What Changed and Why It's Better
Let's break down the improvements:
Focused Components: Each component now has a single responsibility:
UserBasicInfo
handles only basic user dataSocialLinks
manages only social media linksActivityStats
displays only user statisticsAdminActions
handles admin-specific functionalityUserInteractions
manages user engagement actions
Flexible Composition: Now you can easily:
Use components independently where needed
Test each component in isolation
Modify one aspect without touching others
Better Props Management: Each component receives only the props it needs, making it:
Easier to understand what each component requires
Simpler to maintain type safety
More efficient with React's rendering
Real-World Example: A Data Table Component
Here's a practical example of how ISP can help with a common React pattern data table:
// Instead of one massive table interface
interface DataTableProps<T> {
data: T[];
columns: Column[];
sortable?: boolean;
filterable?: boolean;
pagination?: boolean;
pageSize?: number;
onSort?: (column: string) => void;
onFilter?: (filters: Filter[]) => void;
onPageChange?: (page: number) => void;
loading?: boolean;
error?: Error;
selection?: boolean;
onSelect?: (items: T[]) => void;
// ... many more props
}
// Break it down using ISP
interface TableDataProps<T> {
data: T[];
columns: Column[];
}
interface SortableProps {
sortable?: boolean;
onSort?: (column: string) => void;
}
interface FilterableProps {
filterable?: boolean;
onFilter?: (filters: Filter[]) => void;
}
interface PaginationProps {
pagination?: boolean;
pageSize?: number;
onPageChange?: (page: number) => void;
}
interface TableStateProps {
loading?: boolean;
error?: Error;
}
interface SelectionProps<T> {
selection?: boolean;
onSelect?: (items: T[]) => void;
}
// Compose them using higher-order components or hooks
const DataTable = <T extends unknown>(props: TableDataProps<T>) => {
// Base table implementation
};
const withSorting = <T extends unknown>(
WrappedComponent: React.ComponentType<TableDataProps<T>>
) => {
return (props: TableDataProps<T> & SortableProps) => {
// Add sorting functionality
return <WrappedComponent {...props} />;
};
};
const withFiltering = <T extends unknown>(
WrappedComponent: React.ComponentType<TableDataProps<T>>
) => {
return (props: TableDataProps<T> & FilterableProps) => {
// Add filtering functionality
return <WrappedComponent {...props} />;
};
};
// Usage
const SortableTable = withSorting(DataTable);
const FilterableTable = withFiltering(DataTable);
const FullFeaturedTable = withFiltering(withSorting(DataTable));
Common Mistakes to Avoid
While applying ISP, watch out for these pitfalls:
1. Over-segregation
// Too granular!
interface NameProps { name: string; }
interface EmailProps { email: string; }
interface AvatarProps { avatar: string; }
// Better balance
interface UserBasicInfoProps {
name: string;
email: string;
avatar: string;
}
2. Prop Drilling
// Passing admin props through multiple layers
const App = () => (
<UserProfile
adminProps={adminProps}
userProps={userProps}
/>
);
// Better approach: Use Context or composition
const AdminContext = React.createContext<AdminProps | null>(null);
const App = () => (
<AdminContext.Provider value={adminProps}>
<UserProfile userProps={userProps} />
</AdminContext.Provider>
);
3. Premature Abstraction
// Creating interfaces for everything
interface ButtonProps {
onClick: () => void;
children: React.ReactNode;
}
// Use built-in types when they suffice
const Button: React.FC<React.ButtonHTMLAttributes<HTMLButtonElement>> = (props) => (
<button {...props} />
);
When to Apply ISP in Real Projects
ISP isn't always necessary. Here's when you should consider it:
Good Times to Apply ISP:
When components have multiple distinct responsibilities
When different parts of a component change for different reasons
When you find yourself passing many unused props through components
When testing becomes complicated due to component complexity
Maybe Skip ISP When:
Working with simple, focused components
The overhead of separation doesn't provide clear benefits
Components are unlikely to change or be reused
Key Takeaways
Start Simple: Begin with cohesive components and split them only when needed.
Follow Dependencies: Let natural dependencies guide your interface boundaries.
Think Reusability: Design interfaces with reuse in mind, but don't over-abstract.
Consider Testing: If testing becomes complex, it might be time to split interfaces.
Balance Trade-offs: Sometimes a slightly larger interface is better than extreme segregation.
Final Thoughts: Keep Your Interfaces Lean & Focused
The Interface Segregation Principle (ISP) is all about clarity, performance, and maintainability in React.
Avoid bloated interfaces that force components to accept unnecessary props.
Split interfaces wisely to make components easier to reuse.
Improve performance by preventing unnecessary re-renders.
This is not just about following rules blindly; it's about writing components that are easier to understand, test, and maintain. By breaking down large interfaces into focused, purpose-specific ones, we create more flexible and maintainable React applications.
Remember: The goal isn't to have the smallest possible interfaces, but to have interfaces that serve a clear, single purpose. Start with reasonable component sizes and refactor when you see clear benefits from separation.
Keep practicing these patterns, and you'll develop an intuition for when and how to apply ISP in your React projects.
This wraps up our “I” from our SOLID principles in the React series! If you’ve learned something new, share this post with your fellow React developers.
Next, we’ll tackle the Dependency Inversion Principle (DIP)
Until then, keep your React code SOLID, and let’s build something amazing together.
Stay tuned, and happy coding!
We at CreoWis believe in sharing knowledge publicly to help the developer community grow. Let’s collaborate, ideate, and craft passion to deliver awe-inspiring product experiences to the world.
Let's connect:
This article is crafted by Chhakuli Zingare, a passionate developer at CreoWis. You can reach out to her on X/Twitter, LinkedIn, and follow her work on the GitHub.
Top comments (1)
Just amazing, without knowing this principle a always used this principle ( most time properly ) in my projects to mainly decrease complexity, as having too many props are hard to handle and causes many unnecessary re renders, so I always break them into smaller parts and then recombine them in final to reduce complexity and following react main principle that function should only have 1 responsibility, but with this guide I'll be using it more carefully to make React app more efficient