loading...
Cover image for Why apply Open/Closed principles in React component composition?

Why apply Open/Closed principles in React component composition?

shadid12 profile image Shadid Haque Updated on ・6 min read

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>
  );
}

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>
  );
}

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
};

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>
);

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>
  );
}

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;
};

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>
  );
}

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>
  );
}

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>
  );
};

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>
  );
}

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>
  );
};

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>
  );
}

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.

Posted on by:

Discussion

pic
Editor guide
 

When you could continue with this series of resources, I would like to be able to read the complete series, thank you very much for the support in particular that has helped me a lot.

 

Awesome. I love to see how classic principles/best practices are applied in new contexts. Thanks!

Point that I find the most important here is that instead of making component more complex, we can make it more extensible and use it as a building block for other components. Each of them will have a single responsibility and avoid the noise you mention.

The way to do that is not necessarily through higher order components though. It's simpler to create a "hole" in a component by introducing a prop by which we pass nodes. In OrderReport that would be children.

We could implement FastOrder as a standard component that uses OrderReport and extends it with some extras:

const FastOrder = (props) => {
  const [fastTracker, setFastTracker] = React.useState(props.isFastTracked);

  return (
    <OrderReport customer={props.customer}>
      <br />
      <button
        onClick={() => {
          setFastTracker(!fastTracker);
        }}
      >
        Toggle Tracking
      </button>
      {fastTracker ? (
        <div>Fast Tracked Enabled</div>
      ) : (
        <div>Not Fast Tracked</div>
      )}
    </OrderReport>
  );
}

We can repeat this process for the next two components, much like you did, but without burdening ourselves with the concept of higher order components.

Also, what I would do at this point is to make OrderReport a component that's only used to build other components (kinda like an abstract class). Then we would have

const SimpleOrder = (props) => <OrderReport {...props} />

Should the requirements only for the basic order change, we can amend SimpleOrderReport, which doesn't affect the other components!

 

What I like about this is that it reveals how versatile of a building block components are. Much like functions. And I don't think this relationship is an accident: blog.ploeh.dk/2014/03/10/solid-the...!

 

@Wiktor Czajkowski those were some great suggestions. I really appreciate the feedback. I love that pattern you are using to get rid of the HOC. It is always amazing to learn something. Thanks a lot :) :)

 

I am really curious about the Liskov's substitution because I think it's the one that might be more complicated but I imagine, due to React's nature, instead of OOP, would be a compositional solution. Having a more generic component, or a HOC, and creating components based on them.

 

Yes I totally agree. I was recently doing some research on functional design patterns on React and how to use them. In my day job we started experimenting with higher order functional components and higher order functions as hooks as well. Probably will drop a blog post on that soon :)