Learn how solid principles can be used to compose clean scalable frontend applications with React.js
Don’t you dream of writing software that you can send to your grandma!!! :) Every developer dreams of building clean, scalable and reliable software they are proud of. Well maybe not everyone, but if you are one, then you have probably heard of or used SOLID principles. (If not maybe you should read this article)
Here’s a quick intro to SOLID:
In object-oriented computer programming, SOLID is a mnemonic acronym for five design principles intended to make software designs more understandable, flexible and maintainable.
- Single responsibility principle A class or function should only have a single responsibility, that is, only changes to one part of the software's specification should be able to affect the specification of the class.
- Open–closed principle[7] "Software entities ... should be open for extension, but closed for modification."
- Liskov substitution principle[8] "Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program." See also design by contract.
- Interface segregation principle[9] "Many client-specific interfaces are better than one general-purpose interface."[4]
- Dependency inversion principle[10] One should "depend upon abstractions, [not] concretions."[4]
But wait a sec, isn’t javascript a functional language ? how could we apply these pure OOP concepts ? Actually we can. For instance Angular 2+ framework allows us to apply SOLID principles and abstracts lots of functional behaviour of javascript under the hood. So in Angular it feels like we are writing OOP code. React however, is not opinionated and it doesn’t have such abstraction or structures like Angular. Now in React.js we get to decide how we architect our applications. So let’s dig in and see how we can apply SOLID patterns to make our react apps scalable and maintainable. We will also be discussing some of the advanced React component composition patterns here as well.
1. Single Responsibility Principle (SRP)
- Importance of writing single responsibility components
React is Component based. We can build encapsulated components that manage their own state, then compose them to make complex UIs.
Component-based development is productive, easy to manage and maintain. A very complex system can be built relatively easily from specialized and easy to manage pieces. However, if the components are not well designed we cannot reuse and compose them efficiently. Bulky tightly coupled components with many responsibilities only increases technical debt. As our application grows it becomes harder to add new functionality or update existing ones.
When should we break down a component to multiple components ?
Let's take a look at the following UsersComponent
import React from "react";
import ReactDOM from "react-dom";
import axios from "axios";
function UsersComponent() {
const [users, setUsers] = React.useState([]);
React.useEffect(() => {
axios
.get("https://reqres.in/api/users?page=2")
.then(res => setUsers(res.data.data))
.catch(err => console.log(err));
});
return(
<div className="App">
{users.map(aUser => (
<li>
<span>
{aUser.first_name}::{aUser.last_name}
</span>
</li>
))}
</div>
);
}
This component is breaking the SRP. It has two responsibilities calling the api and rendering a list. Although it doesn't look as bad but let's say we get couple more requirements about how the list of user should be rendered. Let's say we would check for if a user has an avatar if not then set them a default avatar. So now out component looks more like this
return (
<div className="App">
{users.map(aUser => (
<li>
<span>
{aUser.first_name}::{aUser.last_name}
{ users.avatar ? (
...Show avatar
...Show some action for user
) : (
....Some Psuedo Code
)}
</span>
</li>
))}
</div>
);
So it is getting cumbersome and this is a good indication that we need to refactor this code. So we create a new UserListComponent
function usersList(props) {
const uploadAvatar = () => {
console.log("implement update avatar");
};
return (
<div>
{props.users.map(aUser => (
<li>
<span>
{aUser.first_name}::{aUser.last_name}
</span>
{aUser.avatar ? (
<img src={aUser.avatar} alt="" />
) : (
<button onClick={uploadAvatar}>Upload avatar</button>
)}
</li>
))}
</div>
);
}
Now if we wanted to add more functionalities to our users list we can do so without worrying about the rest of the application. We can simply modify our existing user list component. So let’s add couple more methods
function usersList(props) {
const uploadAvatar = () => {
console.log("implement update avatar");
};
return (
<div>
{props.users.map(aUser => (
<li>
<span>
{aUser.first_name}::{aUser.last_name}
</span>
{aUser.avatar ? (
<img src={aUser.avatar} alt="" />
) : (
<button onClick={uploadAvatar}>Upload avatar</button>
)}
<button>View Profile</button>
</li>
))}
</div>
);
}
Now if we wanted to add more functionalities to our users list we can do so without worrying about the rest of the application. We can simply modify our existing user list component. So let’s add couple more methods
function usersList(props) {
const uploadAvatar = () => {
console.log("implement update avatar");
};
const viewProfile = id => {
console.log("Route there --->", id);
};
const sendEmail = id => {
console.log("Email", id);
};
const sendSms = id => {
if(isPhoneNumberValid(id)){
console.log("Send SMS", id);
}
};
const isPhoneNumberValid = id => {
// Do phonenumber validation
return true;
}
return (
<div>
{props.users.map(aUser => (
<li>
<span>
{aUser.first_name}::{aUser.last_name}
</span>
{aUser.avatar ? (
<img src={aUser.avatar} alt="" />
) : (
<button onClick={uploadAvatar}>Upload avatar</button>
)}
<button
onClick={() => {
viewProfile(aUser.id);
}}
>
View Profile
</button>
<button onClick={() => sendEmail(aUser.id)}>Send Email</button>
<button onClick={() => sendSms(aUser.id)}>Send SMS</button>
</li>
))}
</div>
);
}
And our return from UsersComponent
now looks like this
return (
<div className="App">
<UsersList users={users} />
</div>
);
We also make sure that all the methods are only responsible for doing one thing. We keep our methods small and compact.
Higher Order Component (HOC)** patterns to separate responsibility and concerns in components
Now lets say things get more complicated. Let’s say based on the type of user we need to impose actions. For example a user with premium subscription will be getting a different type of email than a user who is not a premium member. Also let’s say a premium user is eligible to receive discount coupons with their email sometime. We can see a pattern here. We can reuse the existing methods and add these new methods on top of them. But since inheritance is not really an option is React how could we achieve this (If you want to know more about why inheritance is not an option is react please read the guide here). Well the answer is composition with higher order components.
So let’s compose a higher order component which will have all the user functionality but in addition will also have premium user functionalities.
export const withPremium = BaseUserComponent => props => {
const premiumAction = () => {
console.log("Only Premium Subscribers get it ---->");
};
return <BaseUserComponent {...props} primium premiumAction={premiumAction} />;
};
Once we do that we can compose our UserItem
and wrap it with the new higher order component to have additional functionality. So let's update the code
const PremiumUser = withPremium(UserItem);
function UsersList(props) {
return (
<div>
{props.users.map(aUser => {
if (aUser.id === 8) {
return <PremiumUser user={aUser} />;
} else {
return (
<li>
<UserItem user={aUser} />
</li>
);
}
})}
</div>
);
}
Modify the UserItem
component to return something like below so that only premium users are able to do some additional actions.
return (
<React.Fragment>
<span>
{props.user.first_name}::{props.user.last_name}
</span>
{props.user.avatar ? (
<img src={props.user.avatar} alt="" />
) : (
<button onClick={uploadAvatar}>Upload avatar</button>
)}
<button
onClick={() => {
viewProfile(props.user.id);
}}
>
View Profile
</button>
<button onClick={() => sendEmail(props.user.id)}>Send Email</button>
<button onClick={() => sendSms(props.user.id)}>Send SMS</button>
{props.primium ? (
<button onClick={props.premiumAction}>Premium User</button>
) : null}
</React.Fragment>
);
Neat huh ? Just like in OOP where we use inheritance to extend Objects here we can compose functions/objects in functional programming. Again by doing composition we are ensuring clean, single responsibility components that are easy to maintain and test.
*** NOTE: This post is a work in progress, I am continuously updating the contents. The rest will be available soon. Meantime please follow me if you like this or leave a comment iff you hate it ;) ***
Want to know how to apply open/closed principle in React? click here
Top comments (2)
Your refactored usersList which needs to be UsersList also breaks SRP.
Excellent article ! Thank you