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 (42)
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.
This is a great idea. Curious to know your thoughts on how do we reduce code duplication for the UI composition.
I'm not sure what you mean - duplication in this example or in general?
The best thing you can do is to identify patterns in your app and extract those patterns into new compnents. For example, if you are building a list of cards and every card has an image, header and title. Then later you need that same card in some other place, then you can extract it its own
Card
component.As with regular functions, you need to think about the API design of the
Card
. What props can it take, what use cases should it be able to handle? I like to make a distinction between "presentational components" (that only display data that is given to them so that you can keep them simple) and "data fetching components" (the ones that fetch data from the API and pass it to presentational components).To practise this, you can take a design (from Figma or whatever) and draw boxes around parts of the UI that you think should be a component. The React docs explain how to do this.
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!!
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.
Wow, didn't knew about this
Really nice article. Felt glad someone was doing the good work of reviving old design patterns and applying them into react.
Thank you Hugo, amazing post
Great article. Thanks for sharing 🙏
Love it. Need more quality content like this!
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.
Some comments may only be visible to logged-in visitors. Sign in to view all comments.