Why apply Open/Closed principles in React component composition?

Shadid Haque - Oct 29 '19 - - Dev Community

Have you ever looked at a messy piece of code and just wanted to burn it down? I know I have had 😊. That’s why I started to learn software architecture. I started to think about working on a clean, scalable, reliable code base that makes development fun. After all, implementing new features should be exciting not stressful.

In this article we are going to explore how we can take advantage of composition pattern and apply Open/Close principle (from SOLID principles) to design our applications so that they are easy to work with, expandable and enjoyable to code features.

please note: this is the second part of my React design pattern series with SOLID principles. You can find the first part here

What is Open/Closed Principle?

In object-oriented programming, the open/closed principle states "software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification"; that is, such an entity can allow its behaviour to be extended without modifying its source code.

How do we apply OCP in React?

In OOP languages such as Java or Python this concept is applied through inheritance. This keeps the code DRY and reduces coupling. If you are familiar with Angular 2+ then you know that it is possible to do inheritance in Angular 2+. However, JavaScript is not really a pure Object Oriented language and it doesn’t support classical inheritance like OOP languages such as Java, python or C#. So whenever you are implementing an interface or extending a class in Angular 2+, the framework itself is doing some process in the background and giving you the illusion of writing OOP code. In React we don’t have that luxury. React team encourages functional composition over inheritance. Higher Order Functions are JavaScript's way of reusing code and keeping it DRY.

Let’s look at some code and see how we compose components and how we can follow open/closed principle to write clean, reliable code.
Below we have an App component that is rendering OrderReport. We are passing in a customer object as props.

function App() {
  const customer = {
    name: 'Company A',
    address: '720 Kennedy Rd',
    total: 1000
  }
  return (
    <div className="App">
      <OrderReport customer={customer}/>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Now let’s take a look at our OrderReport Compoennet

function OrderReport(props) {
  return (
    <div>
      <b>{props.customer.name}</b>
      <hr />
      <span>{props.customer.address}</span>
      <br />
      <span>Orders: {props.customer.total}</span>
      {props.children}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

This component here has a little secrect ;). It doesn’t like changes. For instance let’s say we have a new customer object with couple more fields than the first one. We want to render additional information based on our new customer object that is passed as props. So let's take a look at the code below.

const customerB = {
    name: "Company B",
    address: "410 Ramsy St",
    total: 1000,
    isEligible: true,
    isFastTracked: false
};
const customerC = {
    name: "Company C",
    address: "123 Abram Ave",
    total: 1010,
    specialDelivery: true
};
Enter fullscreen mode Exit fullscreen mode

We added 2 new customer objects, they both have couple new extra keys. Let's say based on these keys we are required to render additional html elements in our components. So in our App component we are now returning something like this

return (
    <div className="App">
      <OrderReport customer={customer} />
      <OrderReport customer={customerB} />
      <OrderReport customer={customerC} />
    </div>
);
Enter fullscreen mode Exit fullscreen mode

And we change our OrderReport component accordingly to render additional functionality based on passed props. So our component now looks something like this

function OrderReport(props) {
  const [fastTracker, setFastTracker] = React.useState(props.isFastTracked);
  return (
    <div>
      <b>{props.customer.name}</b>
      <hr />
      <span>{props.customer.address}</span>
      <br />
      <span>Orders: {props.customer.total}</span>
      {props.customer.isEligible ? (
        <React.Fragment>
          <br />
          <button
            onClick={() => {
              setFastTracker(!fastTracker);
            }}
          />
        </React.Fragment>
      ) : null}
      {props.customer.specialDelivery ? (
        <div>Other Logic</div>
      ) : (
        <div>Some option for specialDelivery logic...</div>
      )}
      {props.children}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

As you can see it already started to look very noisy. This is also violating the single responsibility principle. This component is responsible for doing too many tasks now. According to open/closed principle components should be open to extension but closed for modification, but here we are modifying too many logic at once. We are also introducing unwanted complexity in the code. To resolve this let’s create a higher order component to break up this logic.

const withFastTrackedOrder = BaseUserComponent => props => {
  const [fastTracker, setFastTracker] = React.useState(props.isFastTracked);
  const baseElments = (
    <BaseUserComponent customer={props.customer}>
      <br />
      <button
        onClick={() => {
          setFastTracker(!fastTracker);
        }}
      >
        Toggle Tracking
      </button>
      {fastTracker ? (
        <div>Fast Tracked Enabled</div>
      ) : (
        <div>Not Fast Tracked</div>
      )}
    </BaseUserComponent>
  );
  return baseElments;
};
Enter fullscreen mode Exit fullscreen mode

As you can see above that we created withFastTrackedOrder HOC that consumes an OrderReport component and adds in some extra logic and html.

Now all our fast tracked orders logic is encapsulated inside one withFastTrackedOrder component. Here withFastTrackedOrder adding additional functionality and extending our already written logic from OrderReport. Let's revert back our OrderReport to its minimal form like shown below.

function OrderReport(props) {
  return (
    <div>
      <b>{props.customer.name}</b>
      <hr />
      <span>{props.customer.address}</span>
      <br />
      <span>Orders: {props.customer.total}</span>
      {props.children}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

In our App we are doing rendering them like following now

function App() {
  const FastOrder = withFastTrackedOrder(OrderReport);
  return (
    <div className="App">
      <OrderReport customer={customer} />
      <FastOrder customer={customerB} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

So there you have it. We have broken down the logic into two maintainable, clean components. OrderReport is now open for extensions but closed for modification.

Now let's assume our business rule requires us to render some extra html for customers with special orders. Can we extend our OrderReport again. absolutely we can. Let's create another HOC that will compose OrderReport.

const withSpecialOrder = BaseUserComponent => props => {
  return (
      <BaseUserComponent customer={props.customer}>
        <div>I am very special</div>
        {props.children}
      </BaseUserComponent>
  );
};
Enter fullscreen mode Exit fullscreen mode

withSpecialOrder component is consuming the OrderReport and adding the extra html in.
Now in our App we just do the following

function App() {
  const FastOrder = withFastTrackedOrder(OrderReport);
  const SpecialOrder = withSpecialOrder(OrderReport);
  return (
    <div className="App">
      <OrderReport customer={customer} />
      <FastOrder customer={customerB} />
      <SpecialOrder customer={customerC} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Beautiful, isn' it? we have composed our components in small chunks. We have kept them separated by logic and we are not rewriting same logic. All our components are open for extension. We are able to reuse the code and keep it DRY.
Let's take this idea a step further. Let's say now our business allows same day delivery service for some special Orders. We can write another higher order component to wrap our SpecialOrderComponent and add this additional logic in. Remember our components are always open for extension and closed for modification. So with the creation of a new HOC we are extending our existing component's functionality. Let's write this HOC.

const withSameDayDeliver = SpecialOrderComponent => props => {
  return (
    <SpecialOrderComponent customer={props.customer}>
      <div>I am also same day delivery</div>
      {props.children}
    </SpecialOrderComponent>
  );
};
Enter fullscreen mode Exit fullscreen mode

now apply this new HOC to our App like so

function App() {
  const FastOrder = withFastTrackedOrder(OrderReport);
  const SpecialOrder = withSpecialOrder(OrderReport);
  const SameDayDelivery = withSameDayDeliver(withSpecialOrder(OrderReport));
  return (
    <div className="App">
      <OrderReport customer={customer} />
      <FastOrder customer={customerB} />
      <SpecialOrder customer={customerC} />
      <SameDayDelivery customer={customerC} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Now as you can see we have created a pattern of using HOC in a way that they are always open to extension but close for complicated modification. We can add in as many HOC as possible and as our code grows in complexity we can even mix and match these HOCs. This keeps our code simple and enjoyable to work with. It keeps our logic encapsulated so changes dont affect the entire system. It also maintains code sanity in the long run.

The contents of these articles are in progress and I am constantly updating them based on best practices in the industry and my personal experience. Your feedback is crucial, please leave a comment if you have something to say. Please follow me for new articles like this.

You can find the link of previous article of this series here.
Please like this post if you enjoyed it, keeps me motivated :)

Next we will discuss how liskov's substitution is applied in React component architecture. Stay tuned.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .