DEV Community

Cover image for ⚛️ Applying Strategy Pattern in React (Part 1)
Will T.
Will T.

Posted on • Updated on

⚛️ Applying Strategy Pattern in React (Part 1)

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.

Shotgun Surgery Code Smell
(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:

Pricing Cards

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;
Enter fullscreen mode Exit fullscreen mode

And we simply pass the instantiated strategy as a prop to the PricingCard component:

<PricingCard price={7669} strategy={new JapanPriceStrategy()} />
Enter fullscreen mode Exit fullscreen mode

with the props of PricingCard defined as:

interface PricingCardProps {
  price: number;
  strategy: PriceStrategy;
}
Enter fullscreen mode Exit fullscreen mode

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:

Before - Shotgun Surgery

to this:

After - Strategy Pattern

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:


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)

Collapse
 
tanpn profile image
Steven tan Blak • Edited

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!!

Collapse
 
ekeijl profile image
Edwin • Edited

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 a strategy 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:

// translations/data.json
{
  en: { pricing: { discount: "You get a {{price}} discount!" } },
  jp: { pricing: { discount: "{{price}}円引き!" } }
}

// PricingCard.tsx
import { T } from '../components/T';

const PricingCard = () => <div><T key="pricing.discount" value={{ price: 100 }} /></div>;
Enter fullscreen mode Exit fullscreen mode

The same thing is done for formatting currency in the user's locale: the useLocale() hook exposes the formatCurrency() 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 the DiscountMessage component!

<PricingCard>
      <PricingCardHeader />
      <Divider />
      <PricingCardBody price={formatCurrency(price)}>
        <DiscountMessage>
          <T
            t="pricing.discount"
            values={{ discount: formatCurrency(discountedPrice) }}
          />
        </DiscountMessage>
      </PricingCardBody>
    </PricingCard>
Enter fullscreen mode Exit fullscreen mode

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.

Collapse
 
webwelten profile image
Michael Abt

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.

Collapse
 
ekeijl profile image
Edwin

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:

Note here [with Strategy pattern applied] the interface and classes have nothing to do with the UI directly. This logic can be shared in other places in the application or even moved to backend services (if the backend is written in Node, for example).

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.

Thread Thread
 
itswillt profile image
Will T.

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.

Collapse
 
radandevist profile image
Andrianarisoa Daniel

Yes, but how do I organize this code now? I mean the folder structure, where should I place these new classes?

Collapse
 
itswillt profile image
Will T.

I'm gonna have another article talking about folder structures soon, which will fully resolve your question. Please look forward to it.

Collapse
 
radandevist profile image
Andrianarisoa Daniel

Thank you in advance.

Thread Thread
 
itswillt profile image
Will T.

@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-...

Collapse
 
raibtoffoletto profile image
Raí B. Toffoletto

Great writing! 🎉 Looking forward for more.

Collapse
 
aatmaj profile image
Aatmaj

Really nice article. Felt glad someone was doing the good work of reviving old design patterns and applying them into react.

Collapse
 
davidalimazo profile image
Davidalimazo

Thanks Hugo for this great work!

Collapse
 
kuro091 profile image
Kuro091

Love it. Need more quality content like this!

Collapse
 
yuridevat profile image
Julia 👩🏻‍💻 GDE

Great article. Thanks for sharing 🙏

Collapse
 
y4m4to profile image
Yamato Aizawa • Edited

Thank you Hugo, great post!
I have one question. Why is the strategy class made to not accept a price?

Collapse
 
itswillt profile image
Will T.

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.

Collapse
 
y4m4to profile image
Yamato Aizawa

Thank you. That makes sense.

Collapse
 
han profile image
Han

Nice article!

Collapse
 
arunbohra12 profile image
Arun Bohra

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.

Collapse
 
lovepreetsingh profile image
Lovepreet Singh

Wow, didn't knew about this

Collapse
 
borzoomv profile image
Borzoo Moazami

Thank you Hugo, amazing post

Collapse
 
rezk2ll profile image
Khaled Ferjani

Awesome Huy

Collapse
 
anhlalongday profile image
Nong Trần

Being your fan from now :O

Some comments may only be visible to logged-in visitors. Sign in to view all comments.