Skip to content

Instantly share code, notes, and snippets.

@Shadid12
Last active October 22, 2019 22:50
Show Gist options
  • Save Shadid12/3f921d6b60ce94918ca21f33ea143212 to your computer and use it in GitHub Desktop.
Save Shadid12/3f921d6b60ce94918ca21f33ea143212 to your computer and use it in GitHub Desktop.

SOLID principles in React

Part 1. Developing Scalable, Reliable, Clean React Components with Single Responsibility Principle

This article is a part of an ongoing series where we deep dive into SOLID principles and how we can leverage it to better architect our large-scale front-end applications. We discuss how SOLID principles fit in to the Functional Programming Paradigm of JavaScript.

If you are coming from an object oriented programming world (especially Java or C#) you have already heard and used SOLID principles. SOLID is an acronym for 5 important design principles when doing OOP (Object Oriented Programming).

SOLID stands for S – Single Responsibility Principle (SRP in short) O – Open Close Principle L – Liskov Substitution Principle I – Interface Segregation Principle and D – Dependency Inversion Principle

In this article we are going to discuss the S of SOLID. Single Responsibility Principle (SRP) and how it applies to React as well as Functional world of JavaScript. I have further broken down the topic as shown below.

  1. Importance of writing single responsibility components a. Case study: when to break down components

  2. Higher Order Component (HOC) patterns to separate responsibility and concerns in components

    a. Props Proxy techniques in HOC to separate concerns

    b. Render Hijacking techniques to compose components

  3. Curry functions and how to use curried functions and partial application for separation of concerns further

  4. Leveraging React Context for SRP

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

1. a) 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.

2. 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 we use inheritance to extend Objects we can compose functions in functional programming. Again by doing composition we are ensuring clean, single responsibility components that are easy to maintain and test.

WIP

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment