DEV Community

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

⚛️ Applying Strategy Pattern in React (Part 1)

Will T. on December 14, 2022

This article is about a problem many of us encounter in React & Frontend development (sometimes even without realizing that it's a problem): Ha...
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
 
vinsay11 profile image
Winsay vasva

That finds good....keep writing

Collapse
 
nqdai1992 profile image
Nguyễn Quốc Đại

Great article!. I hope read more your article like this :D

Collapse
 
pardeepkashyap5 profile image
Pardeep Kashyap

Great Article, Following you for more like these🥇

Collapse
 
annpham profile image
Pham Ann

so effective! Great read, Hugo!

Collapse
 
anhlalongday profile image
Nong Trần

Being your fan from now :O

Collapse
 
dscheglov profile image
Dmytro Shchehlov

Hey, @itswillt

It is not a good idea to use the abstract class as a type of injectable stratagy. It is better to define the interface. More than, this interface must be defined on the statagy application side, not on the implementation side (see DIP).

Another problem I see is that we are trying to use a design pattern for issue that must be solved in another way.

If we are talking about different cuntries and currencies, it seems we need to talk about a different languages.

So, it means all story related to the amount formatting, discount messages, and even static text inside of the PricingCard and its sub-components are matter of localezation.

So, it is better not to invent a weel, but use old, kind i18.

That means that the final interface of the PricingCard must looks like that:

export type PricingCardProps = {
  price: number;
  discount: number;
  currency: string; // could CurrencyEnum or something like that
  countryCode: string; // or number, or CountryEnum
}

const PricingCard: React.FC<PricingCardProps>;
Enter fullscreen mode Exit fullscreen mode

And all the rest doing with i18next-react (or something similar).

We had text generation logic out of the view, and we got a problem when we met a need to support several languages -- so, it is better to keep all text as much close to be replaced with {t('MESSAGE')} as it is possible from the project beginning.

Collapse
 
polaroidkidd profile image
Daniel Einars

Great read and can be used with hooks as well as opposed to having class components with extend the abstract call

Collapse
 
itswillt profile image
Will T. • Edited

Thanks for your feedback! Great to hear that it was a good read.

Actually, I'm not using a class component here. Also, this can be implemented using hooks, but the logic is quite pure and not coupled to React at all, so I don't want force the logic into a hook.

Collapse
 
verthon profile image
Krzysztof Sordyl

Moreover - the strategy pattern makes it really easy to test as unit test.

Collapse
 
webjose profile image
José Pablo Ramírez Vargas

I would not do any calculation in React. All should be comming from the backend service. If you ditch React, you are ditching business logic. This is a violation of the thin client definition.

Collapse
 
itswillt profile image
Will T. • Edited

By no means in the article am I suggesting that you should put most of your domain business logic in the client side. This is actually a contrived example of how the Shotgun Surgery can happen within a codebase and how the Strategy Pattern helps to solve it. In a frontend application, you will have a lot of application logic regardless, so the Shotgun Surgery is bound to happen if you're not mindful.

Collapse
 
faithfinder profile image
Dmitrii Kartashev

"Strategy" in JS? Ya mad. The pattern exists to bypass the composition limitations of OOP languages, it's not useful in JS

Collapse
 
aminetakha profile image
Amine Takha

Why not creating a config file let's say a JSON file that contains all informations about each country instead of creating components.

Collapse
 
xcmk123 profile image
xcmk123

Hiện tại anh đang làm việc ở công ty nào vậy ạ ?

Collapse
 
elihood profile image
EliHood

Dope & helpful article. We more articles that cover design patterns in a react approach.

Collapse
 
chloevu profile image
Chloe

Love it, love you.

Collapse
 
louis_pham profile image
Louis

Can't wait for the other topic of Hugo