Application of SOLID principles in the development of React-applications
- Transfer
We recently published material on the SOLID methodology. Today we bring to your attention the translation of an article that is devoted to applying the SOLID principles in developing applications using the popular React library.
The author of the article says that here, for the sake of brevity, he does not show the full implementation of some components.

The Single Responsibility Principle tells us that the module should have one and only one reason for change.
Imagine that we are developing an application that displays a list of users in a table. Here is the component code
We have a component in which the list of users is stored. We download this list via HTTP from some server, the list is editable. Our component violates the principle of sole responsibility, as it has more than one reason for change.
In particular, I can see four reasons for changing a component. Namely, the component changes in the following cases:
How to solve these problems? It is necessary, after the reasons for changing a component are identified, to try to eliminate them, to derive them from the source component, creating suitable abstractions (components or functions) for each such reason.
Let's solve the problems of our component by
Now, if we need to change the title, we change the component
Now we are going to solve problems №3 and №4, creating a class
The last problem that we have at number 4 (changing the table that displays the list of users) has been solved by introducing a presentation component into the project
The Open Closed Principle states that software entities (classes, modules, functions) must be open for expansion, but not for modification.
If you look at the component
To bring the program into compliance with this principle, you can use the composition of components .
Take a look at the code of the component
The component
Now let's look at how, using the new component, a list of users is displayed.
Here we expand the behavior of the component
The substitution principle of Barbara Liskov (Liskov Substitution Principle) indicates that the objects in the programs must be replaced by instances of their subtypes without disrupting the correctness of the program.
If this definition seems to you to be too loosely formulated - that’s a stricter version of it.

Barbara Liskov's principle of substitution: if something looks like a duck and quacks like a duck, but needs batteries - probably the wrong abstraction is chosen.
Take a look at the following example:
We have a class
We call this function, passing it objects

Error
What happened? The class object
The principle of substitution has been violated here, since the function
Fixing this problem is easy - just pass the
Interface Segregation Principle indicates that programs should not depend on what they do not need.
This principle is especially relevant in languages with static typing, in which dependencies are explicitly given by interfaces.
Consider an example:
The component
If you write a test for this component and at the same time use TypeScript or Flow, you will have to create an imitation of the object
At first glance, this does not seem to be a problem if you use pure JavaScript, but if TypeScript ever lodges in your code, this will suddenly lead to test failure due to the need to assign all interface properties, even if only some of them are used.
Whatever it was, but the program, which satisfies the principle of interface separation, turns out to be more understandable.
Remember that this principle applies not only to the types of properties passed to the components.
The principle of dependency inversion (Dependency Inversion Principle) tells us that the object of the dependency must be an abstraction, and not something concrete.
Consider the following example:
If we analyze this code, it becomes clear that the component

Component-Function Relationships A
high-level module should not depend on low-level concrete implementations of anything. It must depend on abstraction.
The component

Inversion of dependencies
Here is the implementation of this mechanism.
Now we can say that the component is distinguished by its weak connectivity, since it does not have information about which particular protocol we are using - HTTP, SOAP, or some other. The component does not care at all.
Adherence to the principle of dependency inversion extends our ability to work with code, since we can very easily change the data loading mechanism, and the component
In addition, it simplifies testing, as it is easy to create a function that simulates the function of loading data.
By investing time in writing quality code, you will earn the gratitude of your colleagues and yourself when, in the future, you will have to come across this code again. Implementing SOLID principles in the development of React applications is a worthwhile investment of time.
Dear readers! Do you use the principles of SOLID when developing React-applications?


The principle of sole responsibility (S)
The Single Responsibility Principle tells us that the module should have one and only one reason for change.
Imagine that we are developing an application that displays a list of users in a table. Here is the component code
App
:class App extends Component {
state = {
users: [{name: 'Jim', surname: 'Smith', age: 33}]
};
componentDidMount() {
this.fetchUsers();
}
async fetchUsers() {
const response = await fetch('http://totallyhardcodedurl.com/users');
const users = await response.json();
this.setState({users});
}
render() {
return (
<div className="App">
<header className="App-header">
// тут опущено огромное количество кода заголовка
</header>
<table>
<thead>
<tr>
<th>First name</th>
<th>Last name</th>
<th>Age</th>
</tr>
</thead>
<tbody>
{this.state.users.map((user, index) => (
<tr key={index}>
<td><inputvalue={user.name} onChange={/* update name in the state */}/></td>
<td><inputvalue={user.surname} onChange={/* update surname in the state*/}/></td>
<td><inputvalue={user.age} onChange={/* update age in the state */}/></td>
</tr>
))}
</tbody>
</table>
<button onClick={() => this.saveUsersOnTheBackend()}>Save</button>
</div>
);
}
saveUsersOnTheBackend(row) {
fetch('http://totallyhardcodedurl.com/users', {
method: "POST",
body: JSON.stringify(this.state.users),
})
}
}
We have a component in which the list of users is stored. We download this list via HTTP from some server, the list is editable. Our component violates the principle of sole responsibility, as it has more than one reason for change.
In particular, I can see four reasons for changing a component. Namely, the component changes in the following cases:
- Every time you need to change the title of the application.
- Every time you need to add a new component to the application (page footer, for example).
- Every time when you need to change the mechanism for downloading data about users, for example - the server address or protocol.
- Every time you need to change a table (for example, change the formatting of columns or perform some other action like this).
How to solve these problems? It is necessary, after the reasons for changing a component are identified, to try to eliminate them, to derive them from the source component, creating suitable abstractions (components or functions) for each such reason.
Let's solve the problems of our component by
App
refactoring it. Its code, after splitting it into several components, will look like this:classAppextendsComponent{
render() {
return (
<div className="App">
<Header/>
<UserList/>
</div>
);
}
}
Now, if we need to change the title, we change the component
Header
, and if we need to add a new component to the application, we change the component App
. Here we have solved problems # 1 (changing the header of the application) and problem # 2 (adding a new component to the application). This is done by moving the corresponding logic from the component App
to the new components. Now we are going to solve problems №3 and №4, creating a class
UserList
. Here is his code:classUserListextendsComponent{
static propTypes = {
fetchUsers: PropTypes.func.isRequired,
saveUsers: PropTypes.func.isRequired
};
state = {
users: [{name: 'Jim', surname: 'Smith', age: 33}]
};
componentDidMount() {
const users = this.props.fetchUsers();
this.setState({users});
}
render() {
return (
<div>
<UserTable users={this.state.users} onUserChange={(user) => this.updateUser(user)}/>
<button onClick={() => this.saveUsers()}>Save</button>
</div>
);
}
updateUser(user) {
// обновить сведения о пользователе в состоянии
}
saveUsers(row) {
this.props.saveUsers(this.state.users);
}
}
UserList
- this is our new component container. Thanks to him, we solved problem number 3 (changing the user loading mechanism) by creating function-properties fetchUser
and saveUser
. As a result, now, when we need to change the link used to load the list of users, we turn to the corresponding function and make changes to it. The last problem that we have at number 4 (changing the table that displays the list of users) has been solved by introducing a presentation component into the project
UserTable
, which encapsulates the formation of HTML code and the styling of the table with users.The principle of openness-closure (O)
The Open Closed Principle states that software entities (classes, modules, functions) must be open for expansion, but not for modification.
If you look at the component
UserList
described above, you will notice that if you need to display a list of users in a different format, we will have to modify the method of render
this component. This is a violation of the principle of openness-closeness. To bring the program into compliance with this principle, you can use the composition of components .
Take a look at the code of the component
UserList
that has been refactored:export classUserListextendsComponent{
static propTypes = {
fetchUsers: PropTypes.func.isRequired,
saveUsers: PropTypes.func.isRequired
};
state = {
users: [{id: 1, name: 'Jim', surname: 'Smith', age: 33}]
};
componentDidMount() {
const users = this.props.fetchUsers();
this.setState({users});
}
render() {
return (
<div>
{this.props.children({
users: this.state.users,
saveUsers: this.saveUsers,
onUserChange: this.onUserChange
})}
</div>
);
}
saveUsers = () => {
this.props.saveUsers(this.state.users);
};
onUserChange = (user) => {
// обновить сведения о пользователе в состоянии
};
}
The component
UserList
, as a result of the modification, turned out to be open for expansion, since it displays the child components, which facilitates changing its behavior. This component is closed for modification, as all changes are performed in separate components. We can even deploy these components independently. Now let's look at how, using the new component, a list of users is displayed.
export classPopulatedUserListextendsComponent{
render() {
return (
<div>
<UserList>{
({users}) => {
return <ul>
{users.map((user, index) => <li key={index}>{user.id}: {user.name} {user.surname}</li>)}
</ul>
}
}
</UserList>
</div>
);
}
}
Here we expand the behavior of the component
UserList
by creating a new component that knows how to display a list of users. We can even download more detailed information about each of the users in this new component, without touching the component UserList
, and this is precisely the purpose of refactoring this component.Barbara Liskov substitution principle (L)
The substitution principle of Barbara Liskov (Liskov Substitution Principle) indicates that the objects in the programs must be replaced by instances of their subtypes without disrupting the correctness of the program.
If this definition seems to you to be too loosely formulated - that’s a stricter version of it.

Barbara Liskov's principle of substitution: if something looks like a duck and quacks like a duck, but needs batteries - probably the wrong abstraction is chosen.
Take a look at the following example:
classUser{
constructor(roles) {
this.roles = roles;
}
getRoles() {
returnthis.roles;
}
}
classAdminUserextendsUser{}
const ordinaryUser = new User(['moderator']);
const adminUser = new AdminUser({role: 'moderator'},{role: 'admin'});
functionshowUserRoles(user) {
const roles = user.getRoles();
roles.forEach((role) =>console.log(role));
}
showUserRoles(ordinaryUser);
showUserRoles(adminUser);
We have a class
User
whose constructor accepts user roles. Based on this class, we create a class AdminUser
. After that, we created a simple function showUserRoles
that takes a type object as User
a parameter and displays all the roles assigned to the user to the console. We call this function, passing it objects
ordinaryUser
and adminUser
then we encounter an error.
Error
What happened? The class object
AdminUser
is similar to the class objectUser
. He certainly "quacks" asUser
he has the same methods as inUser
. The problem is the "battery". The fact is that when creating an objectadminUser
, we passed a couple of objects to it, not an array. The principle of substitution has been violated here, since the function
showUserRoles
must work correctly with objects of a classUser
and with objects created on the basis of the heir classes of this class. Fixing this problem is easy - just pass the
AdminUser
arrayto the constructorinstead of the objects:const ordinaryUser = new User(['moderator']);
const adminUser = new AdminUser(['moderator','admin']);
Interface separation principle (I)
Interface Segregation Principle indicates that programs should not depend on what they do not need.
This principle is especially relevant in languages with static typing, in which dependencies are explicitly given by interfaces.
Consider an example:
classUserTableextendsComponent{
...
render() {
const user = {id: 1, name: 'Thomas', surname: 'Foobar', age: 33};
return (
<div>
...
<UserRow user={user}/>
...
</div>
);
}
...
}
classUserRowextendsComponent{
static propTypes = {
user: PropTypes.object.isRequired,
};
render() {
return (
<tr>
<td>Id: {this.props.user.id}</td>
<td>Name: {this.props.user.name}</td>
</tr>
)
}
}
The component
UserTable
renders the component UserRow
, passing to it, in properties, an object with complete information about the user. If we analyze the component code UserRow
, it will turn out that it depends on the object containing all the information about the user, but he only needs the properties id
and name
. If you write a test for this component and at the same time use TypeScript or Flow, you will have to create an imitation of the object
user
with all its properties, otherwise the compiler will generate an error.At first glance, this does not seem to be a problem if you use pure JavaScript, but if TypeScript ever lodges in your code, this will suddenly lead to test failure due to the need to assign all interface properties, even if only some of them are used.
Whatever it was, but the program, which satisfies the principle of interface separation, turns out to be more understandable.
classUserTableextendsComponent{
...
render() {
const user = {id: 1, name: 'Thomas', surname: 'Foobar', age: 33};
return (
<div>
...
<UserRow id={user.id} name={user.name}/>
...
</div>
);
}
...
}
classUserRowextendsComponent{
static propTypes = {
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
};
render() {
return (
<tr>
<td>Id: {this.props.id}</td>
<td>Name: {this.props.name}</td>
</tr>
)
}
}
Remember that this principle applies not only to the types of properties passed to the components.
Principle of dependency inversion (D)
The principle of dependency inversion (Dependency Inversion Principle) tells us that the object of the dependency must be an abstraction, and not something concrete.
Consider the following example:
classAppextendsComponent{
...
async fetchUsers() {
const users = await fetch('http://totallyhardcodedurl.com/stupid');
this.setState({users});
}
...
}
If we analyze this code, it becomes clear that the component
App
depends on the global function fetch
. If we describe the relationship of these entities in the UML language, we get the following diagram.
Component-Function Relationships A
high-level module should not depend on low-level concrete implementations of anything. It must depend on abstraction.
The component
App
does not need to know how to load information about users. In order to solve this problem, we need to invert the dependencies between the componentApp
and the functionfetch
. Below is a UML diagram illustrating this.
Inversion of dependencies
Here is the implementation of this mechanism.
classAppextendsComponent{
static propTypes = {
fetchUsers: PropTypes.func.isRequired,
saveUsers: PropTypes.func.isRequired
};
...
componentDidMount() {
const users = this.props.fetchUsers();
this.setState({users});
}
...
}
Now we can say that the component is distinguished by its weak connectivity, since it does not have information about which particular protocol we are using - HTTP, SOAP, or some other. The component does not care at all.
Adherence to the principle of dependency inversion extends our ability to work with code, since we can very easily change the data loading mechanism, and the component
App
will not change at all. In addition, it simplifies testing, as it is easy to create a function that simulates the function of loading data.
Results
By investing time in writing quality code, you will earn the gratitude of your colleagues and yourself when, in the future, you will have to come across this code again. Implementing SOLID principles in the development of React applications is a worthwhile investment of time.
Dear readers! Do you use the principles of SOLID when developing React-applications?
