Master SOLID Principles in the React Ecosystem
Add to your RSS feed3 February 20254 min read
Table of Contents
Applying SOLID principles in React and React Native ensures better maintainability, scalability, and code clarity. Let's break down each principle with refined code examples and enhanced comments for clarity.
1. Single Responsibility Principle (SRP)
Definition: Each component should have a single responsibility, meaning it should handle only one concern.
Bad Example (Violating SRP)
In this example, the UserList
component is responsible for both fetching and displaying user data, violating SRP.
1 import React, { useEffect, useState } from "react";23 const UserList = () => {4 const [users, setUsers] = useState([]);56 useEffect(() => {7 fetch("https://api.example.com/users")8 .then((res) => res.json())9 .then((data) => setUsers(data))10 .catch((err) => console.error(err));11 }, []);1213 return (14 <ul>15 {users.map((user) => (16 <li key={user.id}>{user.name}</li>17 ))}18 </ul>19 );20 };2122 export default UserList;
Improved Example (Following SRP)
We separate concerns: UserService
handles data fetching, while UserList
is responsible only for rendering.
1 // UserService.ts - Responsible for fetching user data2 export const fetchUsers = async () => {3 try {4 const response = await fetch("https://api.example.com/users");5 return await response.json();6 } catch (error) {7 console.error("Failed to fetch users:", error);8 return [];9 }10 };
1 // UserList.tsx - Responsible only for rendering users2 import React, { useEffect, useState } from "react";3 import { fetchUsers } from "./UserService";45 const UserList = () => {6 const [users, setUsers] = useState([]);78 useEffect(() => {9 fetchUsers().then(setUsers);10 }, []);1112 return (13 <ul>14 {users.map((user) => (15 <li key={user.id}>{user.name}</li>16 ))}17 </ul>18 );19 };2021 export default UserList;
Why this is better?
✅ UserService.ts
handles data fetching, making UserList.tsx cleaner.
✅ Improves testability—fetching logic can be tested separately.
2. Open-Closed Principle (OCP)
Definition: Components should be open for extension but closed for modification.
Bad Example (Violating OCP)
This button component is not extendable; we need to modify it whenever we add a new button style.
1 const Button = ({ label, type }) => {2 let className = "btn";34 if (type === "primary") className += " btn-primary";5 else if (type === "secondary") className += " btn-secondary";67 return <button className={className}>{label}</button>;8 };
Improved Example (Following OCP)
We use className
as a prop, allowing new styles without modifying existing code.
1 const Button = ({ label, className = "" }) => {2 return <button className={`btn ${className}`}>{label}</button>;3 };45 // Usage examples:6 <Button label="Save" className="btn-primary" />7 <Button label="Cancel" className="btn-secondary" />8 <Button label="Custom" className="btn-custom" />
Why this is better?
✅ New button styles can be added without modifying the core component.
✅ More flexible for future design changes.
3. Liskov Substitution Principle (LSP)
Definition: Subtypes must be substitutable for their base types without breaking functionality.
Bad Example (Violating LSP)
Here, SquareButton
extends Button
but removes necessary props (label
), breaking the contract.
1 const Button = ({ label }) => <button>{label}</button>;23 const SquareButton = () => <button>🔲</button>; // Violates LSP
Improved Example (Following LSP)
Now, SquareButton
properly extends Button
and can be used interchangeably.
1 const Button = ({ label, children }) => (2 <button>{label || children}</button>3 );45 const SquareButton = ({ children }) => (6 <Button>{children || "🔲"}</Button>7 );89 // Usage:10 <Button label="Click me" />11 <SquareButton />
Why this is better?
✅ SquareButton
can replace Button anywhere without breaking the UI.
4. Interface Segregation Principle (ISP)
Definition: Components should not be forced to depend on unnecessary props.
Bad Example (Violating ISP)
This form input component has too many props, forcing consumers to provide unnecessary ones.
1 const Input = ({ value, onChange, placeholder, type, errorMessage, maxLength }) => {2 return (3 <div>4 <input value={value} onChange={onChange} placeholder={placeholder} type={type} maxLength={maxLength} />5 {errorMessage && <span className="error">{errorMessage}</span>}6 </div>7 );8 };
Improved Example (Following ISP)
We split the logic into separate components to ensure they only receive necessary props.
1 const TextInput = ({ value, onChange, placeholder }) => (2 <input value={value} onChange={onChange} placeholder={placeholder} type="text" />3 );45 const PasswordInput = ({ value, onChange }) => (6 <input value={value} onChange={onChange} type="password" />7 );89 const ErrorMessage = ({ message }) => message ? <span className="error">{message}</span> : null;
Why this is better?
✅ Each component has only the required props.
✅ Easier to maintain and extend.
5. Dependency Inversion Principle (DIP)
Definition: High-level modules should not depend on low-level modules; both should depend on abstractions.
Bad Example (Violating DIP)
Here, UserList
directly calls fetch
, making it tightly coupled to the API.
1 const UserList = () => {2 useEffect(() => {3 fetch("https://api.example.com/users") // Hardcoded dependency4 .then((res) => res.json())5 .then(setUsers);6 }, []);7 };
Improved Example (Following DIP)
We inject the fetching function, making UserList
independent of API implementation.
1 const UserList = ({ fetchUsers }) => {2 const [users, setUsers] = useState([]);34 useEffect(() => {5 fetchUsers().then(setUsers);6 }, [fetchUsers]);78 return (9 <ul>10 {users.map((user) => (11 <li key={user.id}>{user.name}</li>12 ))}13 </ul>14 );15 };1617 // Inject fetch function18 const fetchUsersFromAPI = async () => {19 const response = await fetch("https://api.example.com/users");20 return await response.json();21 };2223 // Usage24 <UserList fetchUsers={fetchUsersFromAPI} />;
Why this is better?
✅ UserList no longer depends on how users are fetched.
✅ We can easily swap the API without changing UserList.
Conclusion
Applying SOLID principles in React and React Native improves:
✔ Maintainability – Code is easier to update and debug.
✔ Scalability – Components are reusable and extendable.
✔ Testability – Logic is modular and isolated.
By structuring your components correctly, you ensure a cleaner, more efficient codebase. 🚀