Master SOLID principles in React functional components
The article explains SOLID principles applied in React components, namely the Single-responsibility principle (SRP), Open-Closed Principle (OCP), Liskov Substitution principle (LSP), Interface segregation principle (ISP) and Dependency Inversion Principle.
The SRP states that each component should have a single responsibility and be responsible for one thing only. The OCP suggests that software entities should be open for extension but closed for modification, allowing you to extend their behavior without modifying the source code. The LSP states that child components should be substitutable for their parent components without changing the behavior of the application.
The article provides code examples and solutions to apply these principles in React components.
1. Single-responsibility principle (SRP)
In React, the SRP can be applied to components. Each component should have a single responsibility and be responsible for one thing only. For example, a component that renders a user profile should not also be responsible for managing user authentication.
Here is an example of a component that violates the SRP:
javascript
function UserProfile() {
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
setIsLoading(true);
fetchUser().then((data) => {
setUser(data);
setIsLoading(false);
});
}, []);
function fetchUser() {
return fetch('https://api.example.com/user').then((response) => response.json());
}
function handleLogout() {
// code for logging out the user
}
return (
<div>
{isLoading ? (
<p>Loading user profile...</p>
) : (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
<button onClick={handleLogout}>Logout</button>
</div>
)}
</div>
);
}
In this example, the UserProfile component is responsible for fetching the user data and managing the loading state, as well as handling the logout functionality. To apply the SRP, we can break this component into smaller, more specific components:
javascript
function UserAvatar({ user }) {
return <img src={user.avatarUrl} alt={user.name} />;
}
function UserInfo({ user }) {
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
function LogoutButton({ onLogout }) {
return <button onClick={onLogout}>Logout</button>;
}
function UserProfile() {
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
setIsLoading(true);
fetchUser().then((data) => {
setUser(data);
setIsLoading(false);
});
}, []);
function fetchUser() {
return fetch('https://api.example.com/user').then((response) => response.json());
}
function handleLogout() {
// code for logging out the user
}
return (
<div>
{isLoading ? (
<p>Loading user profile...</p>
) : (
<div>
<UserAvatar user={user} />
<UserInfo user={user} />
<LogoutButton onLogout={handleLogout} />
</div>
)}
</div>
);
}
In this updated example, we have separated the user avatar, user info, and logout button into separate components, each with a single responsibility.
2. Open-Closed Principle (OCP)
OCP is a software design principle that states that software entities (such as classes, modules, and functions) should be open for extension but closed for modification. This means that you should be able to extend the behavior of an entity without modifying its source code.
Here is an example of how to apply the OCP in a React component:
Suppose you have a component that displays a list of items and allows the user to filter the items based on some criteria:
javascript
function ItemList({ items, filter }) {
const filteredItems = items.filter(filter);
return (
<ul>
{filteredItems.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
This component violates the OCP because it is not open for extension. If you want to add a new type of filter, you would have to modify the ItemList component.
To apply the OCP, you can make the ItemList component open for extension by passing in a renderItem function as a prop:
javascript
function ItemList({ items, filter, renderItem }) {
const filteredItems = items.filter(filter);
return (
<ul>
{filteredItems.map((item) => (
<li key={item.id}>{renderItem(item)}</li>
))}
</ul>
);
}
Now, the ItemList component is open for extension, because you can pass in a different renderItem function to customize the rendering of each item. For example, you could create a new component that renders a different type of item:
javascript
function FancyItem({ item }) {
return (
<div>
<h2>{item.name}</h2>
<p>{item.description}</p>
<button>{item.price}</button>
</div>
);
}
You can then pass the FancyItem component as the renderItem prop to the ItemList component:
php
<ItemList items={items} filter={someFilter} renderItem={FancyItem} />
This way, you have extended the behavior of the ItemList component without modifying its source code, making it open for extension and closed for modification.
领英推荐
3. Liskov substitution principle (LSP)
In React, the LSP can be applied by ensuring that child components can be substituted for their parent components without changing the behavior of the application.
Here is an example of a component that violates the LSP:
javascript
function Shape({ width, height }) {
return <div style={{ width, height }} />;
}
function Circle({ radius }) {
return <Shape width={radius * 2} height={radius * 2} style={{ borderRadius: '50%' }} />;
}
In this example, the Circle component extends the Shape component, but it changes the behavior of the application by using a border-radius style to create a circle. To apply the LSP, we can create a new component that is specifically designed to render circles:
javascript
function Circle({ radius }) {
return <div style={{ width: radius * 2, height: radius * 2, borderRadius: '50%' }} />;
}
In this updated example, the Circle component no longer extends the Shape component and has its own specific implementation for rendering circles. This ensures that the behavior of the application is not changed when the Circle component is used in place of the Shape component.
4. Interface segregation principle (ISP)
In React, the ISP can be applied by creating smaller, more specific interfaces for components, rather than a single large interface that does too much.
Here is an example of a component that violates the ISP:
javascript
function Button({ color, size, disabled, onClick, children }) {
return (
<button
style={{
backgroundColor: color,
fontSize: size,
opacity: disabled ? 0.5 : 1,
}}
onClick={onClick}
>
{children}
</button>
);
}
In this example, the Button component has multiple props that control its appearance and behavior. To apply the ISP, we can create more specific interfaces for components, like this:
javascript
function ColoredButton({ color, onClick, children }) {
return (
<button
style={{
backgroundColor: color,
}}
onClick={onClick}
>
{children}
</button>
);
}
function DisabledButton({ onClick, children }) {
return (
<button
style={{
opacity: 0.5,
}}
onClick={onClick}
disabled
>
{children}
</button>
);
}
In this updated example, we have created two new components that each have a more specific interface for their use case. This makes the components more reusable and easier to understand.
5. Dependency Inversion Principle
In React, the Dependency Inversion Principle can be achieved through the use of pure components. Pure components are components that receive data and callbacks through props and render the UI based solely on those props. They do not have any internal state and do not rely on any external dependencies.
To apply the Dependency Inversion Principle using pure components, you should:
Here is an example of how you could use pure components to achieve the Dependency Inversion Principle in a React application:
typescript
// Interface module
export interface IUser {
id: number;
name: string;
}
export interface IUserListProps {
users: IUser[];
onSelectUser: (user: IUser) => void;
}
// UserList component
export const UserList: React.FC<IUserListProps> = ({ users, onSelectUser }) => {
return (
<ul>
{users.map(user => (
<li key={user.id} onClick={() => onSelectUser(user)}>
{user.name}
</li>
))}
</ul>
);
};
// Container component
export const UserListContainer: React.FC = () => {
const [users, setUsers] = useState<IUser[]>([]);
useEffect(() => {
fetchUsers().then(users => setUsers(users));
}, []);
const handleSelectUser = (user: IUser) => {
// Handle user selection
};
return <UserList users={users} onSelectUser={handleSelectUser} />;
};
In this example, the UserList component is a pure component that receives data and a callback through props. The UserListContainer component is a container component that manages the state and handles any data manipulation that is required. Both components use the IUserListProps interface to ensure that they are compatible with each other.
In conclusion, the Single-Responsibility Principle (SRP), Open-Closed Principle (OCP), and Liskov Substitution Principle (LSP) are important software design principles that can help to create more maintainable and extensible React components. By following these principles, we can create components that are easier to understand, modify, and reuse, and that can help to create more robust and scalable React applications. By breaking down components into smaller, more specific components with a single responsibility, making them open for extension but closed for modification, and ensuring that child components can be substituted for their parent components without changing the behavior of the application, we can create React components that are both more flexible and more reliable.