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.
Top comments (10)
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 usesOrderReport
and extends it with some extras: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 haveShould the requirements only for the basic order change, we can amend
SimpleOrderReport
, which doesn't affect the other components!@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 :) :)
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...!
Nice, but how would you handle the case when you have same day and special order report?
Hi. I'd create separate components, like
FastTrackingInfo
andSpecialReportInfo
, and then use them in the-Order
components, i.e.Depending on the context of the feature I might also want to rethink the whole thing. But the above would work.
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.
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 :)
Excellent article ! Thank you
Thank you for your nice article! I really love it.