⚛️ Applying Strategy Pattern in React (Part 1)

Will T. - Dec 14 '22 - - Dev Community

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.

. . . . . . . . . . . .