Design patterns are essential when building scalable and maintainable frontend applications. In this article, we will explore how to apply the Strategy Pattern using modern React and TypeScript.
What is the Strategy Pattern?
As your project grows, you often end up with multiple if/else or switch-case blocks. These can become hard to manage and understand, especially when different business rules require different behaviors. They also violate SOLID principles and may trigger warnings such as “too many conditions” in static analysis tools like SonarQube.
The Strategy Pattern helps in such situations by separating behaviors into individual components. Each component handles a specific behavior and shares a common prop structure. You can add a new strategy simply by creating a new component without changing existing logic. This makes your code more maintainable and compatible with the Open/Closed Principle.
Application Structure and Our Sample Scenario
In this example, we will apply the Strategy Pattern to a food ordering application where the user can choose between three payment methods:
- Credit Card
- Meal Card
- Cash
Each method requires different user interaction and UI structure. We will separate these into strategy components.
Project Structure
src/
├── strategies/
│ ├── CreditCardStrategy.tsx
│ ├── MealCardStrategy.tsx
│ ├── CashStrategy.tsx
├── PaymentSelector.tsx
├── types.ts
└── App.tsxtypes.ts – Shared Payment Props
Each payment method will use the same props: the total amount and a callback for successful payment. This ensures type safety and follows the Strategy Pattern idea of interchangeable components.
export interface PaymentProps {
amount: number;
onSuccess: () => void;
}Strategy Components
CreditCardStrategy.tsx – Paying with Credit Card
This component asks the user to enter a card number. The card number is stored in state. When the user clicks the payment button, it simulates a payment and triggers the onSuccess() callback. In a real app, this is where the payment API would be called.
import React, { useState } from "react";
import { PaymentProps } from "../types";
const CreditCardStrategy: React.FC<PaymentProps> = ({ amount, onSuccess }) => {
const [cardNumber, setCardNumber] = useState("");
const handlePay = () => {
console.log("Paid with credit card:", cardNumber);
onSuccess();
};
return (
<div>
<h3>Credit Card Payment: {amount}₺</h3>
<input
type="text"
placeholder="Card Number"
value={cardNumber}
onChange={(e) => setCardNumber(e.target.value)}
/>
<button onClick={handlePay}>Pay</button>
</div>
);
};
export default CreditCardStrategy;MealCardStrategy.tsx – Paying with Meal Card
This strategy doesn’t ask for user input. It assumes a digital wallet or similar service is already connected. The payment is completed by clicking a button.
import React from "react";
import { PaymentProps } from "../types";
const MealCardStrategy: React.FC<PaymentProps> = ({ amount, onSuccess }) => {
const handlePay = () => {
console.log("Paid with meal card");
onSuccess();
};
return (
<div>
<h3>Meal Card Payment: {amount}$</h3>
<button onClick={handlePay}>Pay</button>
</div>
);
};
export default MealCardStrategy;CashStrategy.tsx – Paying with Cash
This component allows the user to write a note for the delivery person (e.g., “Please bring change for 50$”). The note is stored in local state.
import React, { useState } from "react";
import { PaymentProps } from "../types";
const CashStrategy: React.FC<PaymentProps> = ({ amount, onSuccess }) => {
const [note, setNote] = useState("");
const handlePay = () => {
console.log("Cash payment note:", note);
onSuccess();
};
return (
<div>
<h3>Cash Payment: {amount}₺</h3>
<textarea
placeholder="Note to driver"
value={note}
onChange={(e) => setNote(e.target.value)}
/>
<button onClick={handlePay}>Pay</button>
</div>
);
};
export default CashStrategy;PaymentSelector.tsx – Dynamic Strategy Selection
This component selects the correct strategy component depending on the user’s payment method. Using a strategiesobject map helps avoid using if/else or switch.
import CreditCardStrategy from "./strategies/CreditCardStrategy";
import MealCardStrategy from "./strategies/MealCardStrategy";
import CashStrategy from "./strategies/CashStrategy";
import { PaymentProps } from "./types";
export enum PaymentMethod {
CREDIT_CARD = "CREDIT_CARD",
MEAL_CARD = "MEAL_CARD",
CASH = "CASH",
}
interface Props extends PaymentProps {
method: PaymentMethod;
}
const PaymentSelector: React.FC<Props> = ({ method, amount, onSuccess }) => {
const strategies = {
[PaymentMethod.CREDIT_CARD]: CreditCardStrategy,
[PaymentMethod.MEAL_CARD]: MealCardStrategy,
[PaymentMethod.CASH]: CashStrategy,
};
const SelectedComponent = strategies[method];
return <SelectedComponent amount={amount} onSuccess={onSuccess} />;
};
export default PaymentSelector;App.tsx – Selecting the Payment Method
The main App component lets the user select a payment method from a dropdown. The selected method is passed into the PaymentSelector, which handles rendering and logic.
import React, { useState } from "react";
import PaymentSelector, { PaymentMethod } from "./PaymentSelector";
function App() {
const [method, setMethod] = useState<PaymentMethod>(PaymentMethod.CREDIT_CARD);
const handleSuccess = () => {
alert("Payment successful!");
};
return (
<div>
<h2>Select Payment Method</h2>
<select onChange={(e) => setMethod(e.target.value as PaymentMethod)}>
<option value={PaymentMethod.CREDIT_CARD}>Credit Card</option>
<option value={PaymentMethod.MEAL_CARD}>Meal Card</option>
<option value={PaymentMethod.CASH}>Cash</option>
</select>
<hr />
<PaymentSelector method={method} amount={120} onSuccess={handleSuccess} />
</div>
);
}
export default App;Conclusion
In this guide, we implemented the Strategy Pattern in a React + TypeScript app using a real-world payment scenario. Each payment type had its own component and logic, keeping our main code clean and flexible. Instead of messy control flows, we used dynamic selection to render strategies.
This pattern makes your code easy to test, scale, and understand. You can use this same approach for other cases like login flows, pricing logic, content rendering, or localization. Strategy Pattern helps you swap logic without touching the core structure of your app — a powerful tool for any React developer.