JavaScript Development Space

Master SOLID Principles in the React Ecosystem

Add to your RSS feed3 February 20254 min read
Code Like a Pro: Implementing SOLID Design in React Ecosystem

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.

tsx
1 import React, { useEffect, useState } from "react";
2
3 const UserList = () => {
4 const [users, setUsers] = useState([]);
5
6 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 }, []);
12
13 return (
14 <ul>
15 {users.map((user) => (
16 <li key={user.id}>{user.name}</li>
17 ))}
18 </ul>
19 );
20 };
21
22 export default UserList;

Improved Example (Following SRP)

We separate concerns: UserService handles data fetching, while UserList is responsible only for rendering.

tsx
1 // UserService.ts - Responsible for fetching user data
2 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 };
tsx
1 // UserList.tsx - Responsible only for rendering users
2 import React, { useEffect, useState } from "react";
3 import { fetchUsers } from "./UserService";
4
5 const UserList = () => {
6 const [users, setUsers] = useState([]);
7
8 useEffect(() => {
9 fetchUsers().then(setUsers);
10 }, []);
11
12 return (
13 <ul>
14 {users.map((user) => (
15 <li key={user.id}>{user.name}</li>
16 ))}
17 </ul>
18 );
19 };
20
21 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.

tsx
1 const Button = ({ label, type }) => {
2 let className = "btn";
3
4 if (type === "primary") className += " btn-primary";
5 else if (type === "secondary") className += " btn-secondary";
6
7 return <button className={className}>{label}</button>;
8 };

Improved Example (Following OCP)

We use className as a prop, allowing new styles without modifying existing code.

tsx
1 const Button = ({ label, className = "" }) => {
2 return <button className={`btn ${className}`}>{label}</button>;
3 };
4
5 // 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.

tsx
1 const Button = ({ label }) => <button>{label}</button>;
2
3 const SquareButton = () => <button>🔲</button>; // Violates LSP

Improved Example (Following LSP)

Now, SquareButton properly extends Button and can be used interchangeably.

tsx
1 const Button = ({ label, children }) => (
2 <button>{label || children}</button>
3 );
4
5 const SquareButton = ({ children }) => (
6 <Button>{children || "🔲"}</Button>
7 );
8
9 // 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.

tsx
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.

tsx
1 const TextInput = ({ value, onChange, placeholder }) => (
2 <input value={value} onChange={onChange} placeholder={placeholder} type="text" />
3 );
4
5 const PasswordInput = ({ value, onChange }) => (
6 <input value={value} onChange={onChange} type="password" />
7 );
8
9 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.

tsx
1 const UserList = () => {
2 useEffect(() => {
3 fetch("https://api.example.com/users") // Hardcoded dependency
4 .then((res) => res.json())
5 .then(setUsers);
6 }, []);
7 };

Improved Example (Following DIP)

We inject the fetching function, making UserList independent of API implementation.

tsx
1 const UserList = ({ fetchUsers }) => {
2 const [users, setUsers] = useState([]);
3
4 useEffect(() => {
5 fetchUsers().then(setUsers);
6 }, [fetchUsers]);
7
8 return (
9 <ul>
10 {users.map((user) => (
11 <li key={user.id}>{user.name}</li>
12 ))}
13 </ul>
14 );
15 };
16
17 // Inject fetch function
18 const fetchUsersFromAPI = async () => {
19 const response = await fetch("https://api.example.com/users");
20 return await response.json();
21 };
22
23 // Usage
24 <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. 🚀

JavaScript Development Space

© 2025 JavaScript Development Space - Master JS and NodeJS. All rights reserved.