Applying Design Patterns in React: Strategy Pattern
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:
Most of us would probably implement the pricing card 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:
1 2 3 |
<span class="k">import</span> <span class="p">{</span> <span class="nx">Country</span><span class="p">,</span> <span class="nx">Currency</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">../../types</span><span class="dl">'</span><span class="p">;</span> <span class="kd">abstract</span> <span class="kd">class</span> <span class="nx">PriceStrategy</span> <span class="p">{</span> <span class="k">protected</span> <span class="nx">country</span><span class="p">:</span> <span class="nx">Country</span> <span class="o">=</span> <span class="nx">Country</span><span class="p">.</span><span class="nx">AMERICA</span><span class="p">;</span> <span class="k">protected</span> <span class="nx">currency</span><span class="p">:</span> <span class="nx">Currency</span> <span class="o">=</span> <span class="nx">Currency</span><span class="p">.</span><span class="nx">USD</span><span class="p">;</span> <span class="k">protected</span> <span class="nx">discountRatio</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">getCountry</span><span class="p">():</span> <span class="nx">Country</span> <span class="p">{</span> <span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">country</span><span class="p">;</span> <span class="p">}</span> <span class="nx">formatPrice</span><span class="p">(</span><span class="nx">price</span><span class="p">:</span> <span class="kr">number</span><span class="p">):</span> <span class="kr">string</span> <span class="p">{</span> <span class="k">return</span> <span class="p">[</span><span class="k">this</span><span class="p">.</span><span class="nx">currency</span><span class="p">,</span> <span class="nx">price</span><span class="p">.</span><span class="nx">toLocaleString</span><span class="p">()].</span><span class="nx">join</span><span class="p">(</span><span class="dl">''</span><span class="p">);</span> <span class="p">}</span> <span class="nx">getDiscountAmount</span><span class="p">(</span><span class="nx">price</span><span class="p">:</span> <span class="kr">number</span><span class="p">):</span> <span class="kr">number</span> <span class="p">{</span> <span class="k">return</span> <span class="nx">price</span> <span class="o">*</span> <span class="k">this</span><span class="p">.</span><span class="nx">discountRatio</span><span class="p">;</span> <span class="p">}</span> <span class="nx">getFinalPrice</span><span class="p">(</span><span class="nx">price</span><span class="p">:</span> <span class="kr">number</span><span class="p">):</span> <span class="kr">number</span> <span class="p">{</span> <span class="k">return</span> <span class="nx">price</span> <span class="o">-</span> <span class="k">this</span><span class="p">.</span><span class="nx">getDiscountAmount</span><span class="p">(</span><span class="nx">price</span><span class="p">);</span> <span class="p">}</span> <span class="nx">shouldDiscount</span><span class="p">():</span> <span class="nx">boolean</span> <span class="p">{</span> <span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">discountRatio</span> <span class="o">></span> <span class="mi">0</span><span class="p">;</span> <span class="p">}</span> <span class="nx">getDiscountMessage</span><span class="p">(</span><span class="nx">price</span><span class="p">:</span> <span class="kr">number</span><span class="p">):</span> <span class="kr">string</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">formattedDiscountAmount</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">formatPrice</span><span class="p">(</span> <span class="k">this</span><span class="p">.</span><span class="nx">getDiscountAmount</span><span class="p">(</span><span class="nx">price</span><span class="p">)</span> <span class="p">);</span> <span class="k">return</span> <span class="s2">`It's lucky that you come from </span><span class="p">${</span><span class="k">this</span><span class="p">.</span><span class="nx">country</span><span class="p">}</span><span class="s2">, because we're running a program that discounts the price by </span><span class="p">${</span><span class="nx">formattedDiscountAmount</span><span class="p">}</span><span class="s2">.`</span><span class="p">;</span> <span class="p">}</span> <span class="p">}</span> <span class="k">export</span> <span class="k">default</span> <span class="nx">PriceStrategy</span><span class="p">;</span> |
And we simply pass the instantiated strategy as a prop to the PricingCard
component:
1 2 |
<span class="p"><</span><span class="nc">PricingCard</span> <span class="na">price</span><span class="p">=</span><span class="si">{</span><span class="mi">7669</span><span class="si">}</span> <span class="na">strategy</span><span class="p">=</span><span class="si">{</span><span class="k">new</span> <span class="nx">JapanPriceStrategy</span><span class="p">()</span><span class="si">}</span> <span class="p">/></span> |
with the props of PricingCard
defined as:
1 2 3 |
<span class="kr">interface</span> <span class="nx">PricingCardProps</span> <span class="p">{</span> <span class="nl">price</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span> <span class="nl">strategy</span><span class="p">:</span> <span class="nx">PriceStrategy</span><span class="p">;</span> <span class="p">}</span> |
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.
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.
Source: https://dev.to/itshugo/applying-design-patterns-in-react-strategy-pattern-enn
