SOLID Principles in React
Overview
SOLID is an acronym for five design principles that help make software designs more understandable, flexible, and maintainable. Let's explore how these principles apply to React development.
Table of Contents
- Single Responsibility Principle (SRP)
- Open-Closed Principle (OCP)
- Liskov Substitution Principle (LSP)
- Interface Segregation Principle (ISP)
- Dependency Inversion Principle (DIP)
Single Responsibility Principle
A component should have only one reason to change.
Bad Practice ❌
const UserDashboard = () => {
const [user, setUser] = useState(null);
const [orders, setOrders] = useState([]);
useEffect(() => {
// Fetching user data
fetch('/api/user')
.then(res => res.json())
.then(setUser);
// Fetching orders
fetch('/api/orders')
.then(res => res.json())
.then(setOrders);
}, []);
const handleUpdateProfile = (data) => {
// Update profile logic
};
const handleOrderCancel = (orderId) => {
// Cancel order logic
};
return (
<div>
<UserProfile user={user} onUpdate={handleUpdateProfile} />
<OrderHistory orders={orders} onCancel={handleOrderCancel} />
</div>
);
};
Good Practice ✅
// UserProfileContainer.jsx
const UserProfileContainer = () => {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser().then(setUser);
}, []);
return <UserProfile user={user} />;
};
// OrderHistoryContainer.jsx
const OrderHistoryContainer = () => {
const [orders, setOrders] = useState([]);
useEffect(() => {
fetchOrders().then(setOrders);
}, []);
return <OrderHistory orders={orders} />;
};
// Dashboard.jsx
const Dashboard = () => (
<div>
<UserProfileContainer />
<OrderHistoryContainer />
</div>
);
Open-Closed Principle
Software entities should be open for extension but closed for modification.
Bad Practice ❌
const Button = ({ type, children }) => {
switch (type) {
case 'primary':
return <button className="bg-blue-500">{children}</button>;
case 'secondary':
return <button className="bg-gray-500">{children}</button>;
case 'danger':
return <button className="bg-red-500">{children}</button>;
default:
return <button>{children}</button>;
}
};
Good Practice ✅
const Button = ({ className, children, ...props }) => (
<button className={className} {...props}>
{children}
</button>
);
// Usage
const PrimaryButton = (props) => (
<Button className="bg-blue-500" {...props} />
);
const SecondaryButton = (props) => (
<Button className="bg-gray-500" {...props} />
);
Liskov Substitution Principle
Subtypes must be substitutable for their base types.
Bad Practice ❌
class Animal extends React.Component {
speak() {
return "Some sound";
}
render() {
return <div>{this.speak()}</div>;
}
}
class Cat extends Animal {
speak() {
return "Meow";
}
}
class Fish extends Animal {
// Violates LSP as Fish can't speak
speak() {
throw new Error("Fish can't speak!");
}
}
Good Practice ✅
interface AnimalProps {
makeSound: () => string;
}
const Animal: React.FC<AnimalProps> = ({ makeSound }) => (
<div>{makeSound()}</div>
);
const Cat = () => (
<Animal makeSound={() => "Meow"} />
);
const Fish = () => (
<Animal makeSound={() => "Blub"} />
);
Interface Segregation Principle
Clients should not be forced to depend on interfaces they do not use.
Bad Practice ❌
interface UserProps {
name: string;
email: string;
address: string;
paymentDetails: object;
orderHistory: array;
preferences: object;
}
const UserProfile: React.FC<UserProps> = ({
name,
email,
address,
paymentDetails,
orderHistory,
preferences
}) => {
// Component using all props
};
Good Practice ✅
interface UserBasicInfo {
name: string;
email: string;
}
interface UserAddressInfo {
address: string;
}
interface UserPaymentInfo {
paymentDetails: object;
}
const UserProfile: React.FC<UserBasicInfo> = ({ name, email }) => {
// Only uses basic info
};
const UserAddress: React.FC<UserAddressInfo> = ({ address }) => {
// Only uses address info
};
const UserPayment: React.FC<UserPaymentInfo> = ({ paymentDetails }) => {
// Only uses payment info
};
Dependency Inversion Principle
High-level modules should not depend on low-level modules. Both should depend on abstractions.
Bad Practice ❌
const UserList = () => {
const [users, setUsers] = useState([]);
useEffect(() => {
// Direct dependency on fetch implementation
fetch('/api/users')
.then(res => res.json())
.then(setUsers);
}, []);
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
};
Good Practice ✅
// api.ts
interface UserAPI {
getUsers: () => Promise<User[]>;
}
// Implementation
const apiClient: UserAPI = {
getUsers: () => fetch('/api/users').then(res => res.json())
};
// Hook
const useUsers = (api: UserAPI) => {
const [users, setUsers] = useState([]);
useEffect(() => {
api.getUsers().then(setUsers);
}, [api]);
return users;
};
// Component
const UserList = ({ api }) => {
const users = useUsers(api);
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
};
Best Practices for SOLID in React
1. Component Organization
- Create small, focused components
- Use container/presenter pattern
- Separate business logic from presentation
- Use custom hooks for reusable logic
2. Props Design
- Keep props minimal and focused
- Use TypeScript for better interface definitions
- Avoid prop drilling with Context or state management
- Use composition over inheritance
3. State Management
- Use appropriate state management tools
- Keep state close to where it's used
- Implement clear data flow patterns
- Use reducers for complex state logic
4. Testing
- Write unit tests for isolated components
- Use integration tests for component interactions
- Mock dependencies appropriately
- Test business logic separately from UI
Common Anti-patterns to Avoid
- Huge components with multiple responsibilities
- Tight coupling between components
- Direct API calls in components
- Prop drilling through many levels
- Complex inheritance hierarchies
- Mixed concerns in single components
Tools and Libraries that Help Follow SOLID
- TypeScript - For better interface definitions
- ESLint - For enforcing code patterns
- React Testing Library - For behavior-driven tests
- Redux Toolkit - For organized state management
- React Query - For data fetching abstraction
Conclusion
Following SOLID principles in React leads to:
- More maintainable code
- Better testability
- Easier refactoring
- Cleaner component architecture
- More reusable components
- Better separation of concerns