How to apply SOLID principles in React applications

In this article, we are going to look at the SOLID principles and how to apply them in the React.

For the sake of simplicity, I will not provide the full implementation of some components. So let’s begin.


Single Responsibility Principle

First is the Single responsibility principle. This principle tells us that module should have one and only one reason to change.

Let’s imagine we are trying to build an application which displays users in a table.

We have a component which has a user list in the state. We are fetching users from some HTTP endpoint and each user is editable. This component violates the Single Responsibility Principle because it has more than 1 reason to change.

I can see these reasons.

  1. Every time I want to change the header of application.
  2. Every time I want to add a new component to the application (e.g. footer).
  3. Every time I want to change the user fetching mechanism, for example, the address of endpoint or protocol.
  4. Every time I want to change the user list table (e.g. column styling, etc…)

Solution. After you identify your reasons to change try to eliminate them by creating a suitable abstraction (component, function, …) for each reason.

Let’s try to fix that problem. Lets go refactor the App component.

Now, when we want to change the header we change the Header component and when we want to add a new component we change the App component. We solved problem 1 (change the header of application) and 2 (add a new component to the application) by moving that logic from the App component to the new components. Let’s solve the problem 3 and 4.

This is our new container component UserList. We solved problem 3 (change the user fetching mechanism) by creating props functions fetchUser and saveUser. So when we want to change the HTTP endpoint we go to the function (save/fetch)User and change that there.

The last problem 4 (change the user list table) was resolved by creating a simple presentation component UserTable which encapsulated the HTML and styling of the user table.


Open Closed Principle

This principle states “software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification”.

If you look at the UserList component above you can notice that if we want to display users in a different format, we have to modify the UserList’s render method. This is a violation of this principle.

We can obey this principle by using Component Composition.

Look at the refactored UserList component below.

We modified the UserList component in a way that it is open for extension because it renders its children and therefore it is easy to extend its behavior. It is closed for modification because all necessary changes will be done in different components. We can even deploy this component independently.

Lets look how would we display users in a list by using our new component.

We extended the UserList behavior by creating a new component which knows how to display users. We can even fetch new detailed information about each user in this new component without even touching UserListcomponent and that was the goal.

Liskov Substitution Principle

“objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.”

Or if you don’t mind more accurate description.

Look at the example below.

We have a class User which accept roles in the constructor. Then, we created an AdminUser class which is the User derivative.

After that, we have created a simple function showUserRoles which accept user as a parameter and prints all of the user’s roles to the console.

But after we called the showUserRoles function with ordinaryUser and adminUser instances, it crashed.

And why? The AdminUser looks like the User. It definitely “quacks” like the User because it has the same methods. The problem was with the “batteries”. Because, when creating the admin user, we created an object of roles instead of an array.

We violated the Liskov Substitution Principle because the showUserRoles function should work correctly with the User and its derivatives!

Fix is simple, we will just create an array of roles instead of an object.


Interface Segregation Principle

This principle tells us that we should not depend on things we don’t need.

This principle applies especially on static types languages because your dependencies are explicitly defined by interfaces.

Let’s bring an example.

UserTable component renders UserRow component while passing whole user object to its props. When you look at the UserRow component, it depends on the whole user but only cares about user’s id and name.

When you would write a test for this component in Typescript or Flow, you have to mock the whole user because otherwise, your compiler will fail.

At first, it does not seem a problem if you are using vanilla Javascript, but someday, you will add Typescript to your code base and suddenly it will break all your tests because you would have to assign all properties from interfaces even if you are using only half of them.

But still, it is more descriptive this way.

Keep in mind that this principle does not apply only on prop-types.

Dependency Inversion Principle

This principle tells us that we should depend upon abstractions, not concretions.

Let’s look at the example.

If you look at this code you will see that component App depends on the concretion, more specifically global fetch. In UML world this relation would look like this.

A high-level module should not depend on low-level details, both should depend on abstraction.

The App should not know how to fetch users. In order to fix that problem, we have to inverse the dependencies between App component and fetch so the UML diagram will look like this.

And the implementation.

We can say that App is loosely coupled because it has no knowledge whether we are using HTTP, SOAP or even powerpoint protocol. It does not care.

It gives us power because we can easily change the fetching method and the App component will not change a bit!

And testing is really simple, we can easily mock these fetching functions.


Summary

Invest your time to create a better code, your colleagues and your future self will thank you for that.