How to Use the Strategy Pattern in JavaScript
Every frontend developer has written a function that started with one if statement and slowly turned into twenty.
It usually begins with something harmless.
You need to validate an email.
Then someone asks for phone number validation.
Next comes passwords, usernames, ZIP codes, country-specific rules, feature flags, and suddenly one function contains the business logic for half the application.
The code still works, but every new requirement makes it harder to read, test, and maintain.
The Strategy Pattern offers a much cleaner solution.
Instead of expanding one giant conditional statement, you split each behavior into an independent strategy and choose the correct one at runtime.
The Problem with Large if-Else Chains
Imagine you’re building a user registration form.
A common implementation looks something like this.
function validate(value, type) {
if (type === "email") {
return /\S+@\S+\.\S+/.test(value)
? ""
: "Invalid email address";
}
if (type === "phone") {
return /^\+?[1-9]\d{7,14}$/.test(value)
? ""
: "Invalid phone number";
}
if (type === "password") {
return value.length >= 8
? ""
: "Password is too short";
}
return "";
}The implementation works.
The problem is scalability.
Every new validation rule requires modifying the same function.
Soon dozens of unrelated conditions become tightly coupled, making even small changes risky.
What Is the Strategy Pattern?
The Strategy Pattern encapsulates multiple algorithms behind a common interface.
Instead of asking a function to decide how something should happen, you provide the behavior you want to execute.
Rather than writing:
if this...
else if that...
else...you write:
Select strategy
↓
Execute strategyEach strategy becomes a small, reusable function with a single responsibility.
Refactoring Validation
Instead of putting every rule inside one function, separate them into individual validators.
const validators = {
required: value =>
value.trim()
? ""
: "This field is required",
email: value =>
/^\S+@\S+\.\S+$/.test(value)
? ""
: "Invalid email address",
phone: value =>
/^\+?[1-9]\d{7,14}$/.test(value)
? ""
: "Invalid phone number",
minLength: length =>
value =>
value.length >= length
? ""
: `Minimum ${length} characters`,
};Now the validation engine becomes extremely small.
function validate(value, rules) {
for (const rule of rules) {
const strategy =
typeof rule === "function"
? rule
: validators[rule];
const error = strategy(value);
if (error) {
return error;
}
}
return "";
}Using it is straightforward.
validate(
"john@example.com",
["required", "email"]
);
validate(
"abc",
[
"required",
validators.minLength(8),
]
);Notice what changed.
Adding a new validation rule never requires touching the validation engine.
You simply register another strategy.
This follows one of the core principles of good software design:
Open for extension, closed for modification.
React Integration
This approach works naturally with modern React form libraries.
For example:
<Form.Item
name="email"
rules={[
{
validator: (_, value) =>
validators.email(value),
},
]}
>
<Input />
</Form.Item>The form doesn’t care how email validation works.
It simply executes the selected strategy.
The same validators can also be reused in APIs, custom hooks, or server-side validation.
Strategy Pattern for Status Mapping
Validation isn’t the only place where developers overuse conditional logic.
Status mapping is another common example.
Many applications receive numeric status codes from an API.
Instead of writing multiple conditional statements throughout the UI, create a configuration object.
const ORDER_STATUS = {
pending: {
label: "Pending",
color: "orange",
},
paid: {
label: "Paid",
color: "green",
},
cancelled: {
label: "Cancelled",
color: "gray",
},
};Now rendering becomes much simpler.
const status =
ORDER_STATUS[order.status];
<Tag color={status.color}>
{status.label}
</Tag>Adding a new status becomes a single-line change.
No rendering logic needs to be modified.
This pattern also works well for:
- user roles
- notification types
- feature flags
- payment states
- API response codes
Environment-Specific Behavior
Different environments often require completely different implementations.
During development you may want verbose console logging.
Production applications usually send errors to monitoring services instead.
Instead of adding environment checks everywhere, provide different strategies.
// devLogger.ts
export const logger = {
error: console.error,
warn: console.warn,
};// productionLogger.ts
export const logger = {
error(message, extra) {
Sentry.captureMessage(
message,
{ extra }
);
},
warn() {},
};Then load the appropriate implementation.
const logger = await import(
`./loggers/${import.meta.env.MODE}.ts`
);
logger.error("Something failed");Modern bundlers can tree-shake unused implementations, keeping production bundles smaller.
Other Great Use Cases
Once you start looking for them, Strategy Pattern opportunities appear everywhere.
Formatting values:
Date formatter
Currency formatter
File size formatter
Percentage formatterAuthentication:
Password login
OAuth
Magic links
PasskeysPayments:
Stripe
PayPal
Square
AdyenSearch algorithms:
Exact match
Fuzzy search
Full-text search
Semantic searchInstead of branching through multiple implementations, choose the appropriate strategy.
Benefits
Using the Strategy Pattern provides several advantages.
Smaller functions
Each strategy has one responsibility.
Better testing
Strategies can be tested independently.
Easy extension
Adding new behavior doesn’t require modifying existing logic.
Reusable code
The same strategies can be shared across multiple features.
Cleaner architecture
Business logic becomes separated from decision-making logic.
When Not to Use It
Not every if statement should become a strategy.
If you only have two simple conditions that are unlikely to grow, introducing extra abstraction may make the code harder to understand.
The Strategy Pattern shines when:
- new behaviors are added regularly
- multiple implementations share the same interface
- business rules change frequently
- different environments require different behavior
Otherwise, a simple conditional may be perfectly acceptable.
Final Thoughts
The Strategy Pattern is one of the most practical design patterns in frontend development because it solves a problem every growing application eventually faces.
Conditional logic scales poorly.
Strategies scale naturally.
Instead of expanding one function every time requirements change, encapsulate each behavior inside its own strategy and let configuration decide which one should run.
The result is cleaner code, easier testing, and a codebase that remains maintainable as your application grows.