This article is about a problem many of us encounter in React & Frontend development (sometimes even without realizing that it's a problem): Having a piece of logic implemented throughout different components, hooks, utils, etc.
Let's dive into the problem details and how to solve it. As the title suggests, we're going to use the Strategy Pattern to solve it.
The problem: Shotgun Surgery
Shotgun Surgery is a code smell where making any modifications requires making many small changes to many different places.
(image source: https://refactoring.guru/smells/shotgun-surgery)
How can this happen in a project? Let's imagine we need to implement pricing cards for a product, and we adjust the price, the currency, the discount strategy and the messages based on where the client is coming from:
In this contrived example, without the existence of localization, the pricing card might be implemented as follows:
- Components:
PricingCard
,PricingHeader
,PricingBody
. - Utility functions:
getDiscountMessage
(in utils/discount.ts),formatPriceByCurrency
(in utils/price.ts). - The
PricingBody
component also calculates the final price.
Here's the full implementation:
Now let's imagine we need to change the pricing plan for a country, or add a new pricing plan for another country. What will you have to do with the above implementation? You'll have to at least modify 3 places and add more conditionals to the already messy if-else
blocks:
- Modify the
PricingBody
component. - Modify the
getDiscountMessage
function. - Modify the
formatPriceByCurrency
function.
If you've already heard of S.O.L.I.D, we're already violating the first 2 principles: The Single Responsibility Principle & The Open-Closed Principle.
The solution: Strategy Pattern
The Strategy Pattern is quite straightforward. We can simply understand that each of our pricing plans for the countries is a strategy. And in that strategy class, we implement all the related logic for that strategy.
Suppose you are familiar with OOP, we can have an abstract class (PriceStrategy
) that implements the shared/common logic, and then a strategy with different logic will inherit that abstract class. The PriceStrategy
abstract class looks like this:
import { Country, Currency } from '../../types';
abstract class PriceStrategy {
protected country: Country = Country.AMERICA;
protected currency: Currency = Currency.USD;
protected discountRatio = 0;
getCountry(): Country {
return this.country;
}
formatPrice(price: number): string {
return [this.currency, price.toLocaleString()].join('');
}
getDiscountAmount(price: number): number {
return price * this.discountRatio;
}
getFinalPrice(price: number): number {
return price - this.getDiscountAmount(price);
}
shouldDiscount(): boolean {
return this.discountRatio > 0;
}
getDiscountMessage(price: number): string {
const formattedDiscountAmount = this.formatPrice(
this.getDiscountAmount(price)
);
return `It's lucky that you come from ${this.country}, because we're running a program that discounts the price by ${formattedDiscountAmount}.`;
}
}
export default PriceStrategy;
And we simply pass the instantiated strategy as a prop to the PricingCard
component:
<PricingCard price={7669} strategy={new JapanPriceStrategy()} />
with the props of PricingCard
defined as:
interface PricingCardProps {
price: number;
strategy: PriceStrategy;
}
Again, if you know OOP, not only we're using Inheritance, but we're also using Polymorphism here.
Here's the full implementation of the solution:
And let us ask the same question again: How do we add a new pricing plan for a new country? With this solution, we simply need to add a new strategy class, and we don't need to modify any of the existing code. By doing so, we're satisfying S.O.L.I.D as well.
Conclusion
So, by detecting a code smell - Shotgun Surgery - in our React codebase, we have applied a design pattern - Strategy Pattern - to solve it. Our code structure went from this:
to this:
Now our logic lives in one place and is no longer spread throughout many places anymore. Do note that this whole article revolves around a contrived example. Practically, the Strategy Pattern can be implemented in simpler ways (using objects instead of classes). Please check out part 2 of this series:
⚛️ Applying Strategy Pattern in React (Part 2)
Will T. ・ Mar 9
If you're interested in design patterns & architectures and how they can be used to solve problems in the Frontend world, make sure to give me a like & a follow.
Top comments (40)
Scrolling to find something good these day is hard, but I have to log in and save this article because it's worth spending on. Ty Hugo!!
I would like to propose a different solution to this issue, which uses composition with React components (and a bit of restructuring of the original code) to create specialized UI per locale.
(used Google Translate so translations may be poor, sorry!)
Please refer to
./src/feature/pricing/JapanPricing.tsx
to see how clean the solution is using component composition.I agree that having business logic mixed in with the components (as in the original example) is problematic and should be extracted from components to make it easier to test. However, in your proposed solution, the logic is still somewhat distributed among several components and it requires passing down the strategy multiple components. I'm not a big fan of using these type of abstractions as it is bad for reusability of components (I want to apply
<PricingCard>
in a different context, but I always need to pass astrategy
prop).One hiatus in your examples is that you do not cover translation of text. Translation is a concern that should be handled separately from your business logic and presentation.
In a real life React app you would handle this using something like react-i18next. I implemented a very rudimentary version of a translation framework that supports translation bundles and string interpolation. This means you can use the
<T>
component to translate a key for you and also pass in variables:The same thing is done for formatting currency in the user's locale: the
useLocale()
hook exposes theformatCurrency()
function that formats an number based on the user's locale. I think this is the correct way to separate "localisation" concerns from "presentation" / "business logic" concerns.The issue that remains is creating a specialized UI that shows an additional message about discounts for a specific locale. This is where React shines with component composition, in my opinion. I make a distinction between "UI components", that take simple input props and render them to the screen, and "container components" that compose a more complex UI and apply some conditional rendering . I make use of the
children
prop to make component composition easier.In my final solution, I created
JapanPricing.tsx
where I compose the UI with the<PricingCard>
and<DiscountMessage>
components. This way I don't need any additional logic to determine whether a discount should be applied (strategy.shouldDiscount()
). I just know I need it here so I render theDiscountMessage
component!You could move the
<T>
component inside the<DiscountMessage>
component, among other things. The way you design your component API depends on your needs, how reusable the component needs to be. I mainly wanted to showcase how useful composition can be in this example!The logic for calculating the discount price is in a separate module, yet still colocated with the other modules related to pricing. There is a little bit of code duplication for composing the UI, but this is code that is simple to read and to throw away when it's no longer needed. Making a change is simple as we separate UI concerns from business logic.
Hey Edwin,
thanks for your elaborate answer and the elegant solution. In my opinion it is way easier to use and understand. Also it seems to prepare better for future feature changes or individual adaptions (translations, different UI etc.), things that happen in web dev all the time. And lastly, it feels way more like React and Javascript is intended to use.
In fact, I wouldn't event advise using the solution from the article. I'm surprised that I'm not reading more concerns here.
I recently found another article about this exact same topic and they make a valid argument for the Strategy pattern that is not mentioned in Hugo's article:
So it makes sense to apply the Strategy pattern if you want to extract the business logic so you can easily test it in isolation or apply it in a completely different context (NodeJS backend, Vue, etc). However, it still does not feel very React-ish and it adds some complexity (having to pass in the Strategy object everywhere). These are the trade-offs you need to consider.
Hi, thanks for the comments. I need to clarify that this is a contrived and straightforward example that I came up with to demonstrate the usage of the Strategy Pattern. Any sane developers would use i18n to solve the original problem, but that's not the point.
I'll put a heads-up in the article so that people are not misled.
Yes, but how do I organize this code now? I mean the folder structure, where should I place these new classes?
I'm gonna have another article talking about folder structures soon, which will fully resolve your question. Please look forward to it.
Thank you in advance.
@radandevist
Sorry for the very late response. I kinda forgot to write the article, but here it is at last: dev.to/itswillt/folder-structures-...
Great writing! 🎉 Looking forward for more.
Really nice article. Felt glad someone was doing the good work of reviving old design patterns and applying them into react.
Thanks Hugo for this great work!
Love it. Need more quality content like this!
Great article. Thanks for sharing 🙏
Thank you Hugo, great post!
I have one question. Why is the strategy class made to not accept a price?
It depends on how you define a "strategy". In this example, the scope of a strategy is about how the price should be discounted and what the discount message is, etc. Therefore, it doesn't need to know the exact price itself.
You can define the price inside the strategy class. However, you'd have to reinstantiate the class every time you want to change the price. That'd be a little bit painful I think.
Thank you. That makes sense.
Nice article!
Really nice. I was hoping to implement something like this for a long time but had no idea. Although, my usecases are different but this article will surely help. 😊 Thanks.
Wow, didn't knew about this
Thank you Hugo, amazing post
Awesome Huy
That finds good....keep writing
Some comments may only be visible to logged-in visitors. Sign in to view all comments.