Strategy Pattern in React (with AI Model Example)
The Strategy Pattern is a behavioral design pattern that lets you define a family of algorithms, encapsulate each one, and make them interchangeable at runtime.
Instead of writing large if/else or switch statements, you delegate behavior to separate “strategy” implementations.
In this article, we’ll implement the Strategy Pattern in React using a practical example: building an AI chat app that supports multiple AI models.
The Problem: Tightly Coupled if/else Logic
Imagine we’re building a chat app that supports multiple AI models:
- GPT-4.5
- Claude 3
- Gemini
A common beginner implementation might look like this:
type ModelType = "gpt-4.5" | "claude-3" | "gemini";
interface Props { model: ModelType; message: string;}
export function Chat({ model, message }: Props) { const handleSend = async () => { if (model === "gpt-4.5") { console.log("Calling GPT 4.5", message); } else if (model === "claude-3") { console.log("Calling Claude 3", message); } else if (model === "gemini") { console.log("Calling Gemini", message); } else { throw new Error("Unsupported model"); } };
return <button onClick={handleSend}>Send</button>;}Why This Is Bad
- Adding a new model means adding another
else if - Business logic is tightly coupled to the component
- Violates the Open/Closed Principle
- Hard to scale and maintain
We need something more flexible.
Applying the Strategy Pattern
The Strategy Pattern generally has four steps:
- Define a common interface
- Create separate strategies
- Register strategies (mapping)
- Use a strategy selector (factory)
Step 1: Define the Strategy Interface
We first define a contract that all AI models must follow.
export interface ChatStrategy { sendMessage(message: string): Promise<string>; editMessage?(messageId: string, newMessage: string): Promise<string>; deleteMessage?(messageId: string): Promise<void>;}This ensures every model follows the same structure.
Step 2: Create Separate Strategies
Now we separate each model’s business logic.
OpenAI Strategy
export const openAIStrategy: ChatStrategy = { async sendMessage(message) { console.log("Calling OpenAI send...", message); return "OpenAI response"; },
async editMessage(messageId, newMessage) { console.log("Editing message in OpenAI:", messageId, newMessage); return "Edited OpenAI response"; },
async deleteMessage(messageId) { console.log("Deleting message in OpenAI:", messageId); },};Claude Strategy
export const claudeStrategy: ChatStrategy = { async sendMessage(message) { console.log("Calling Claude send...", message); return "Claude response"; },
async editMessage(messageId, newMessage) { console.log("Claude editing message:", messageId, newMessage); return "Claude edited response"; },};Gemini Strategy
export const geminiStrategy: ChatStrategy = { async sendMessage(message) { console.log("Calling Gemini send...", message); return "Gemini response"; },};Now each model is completely isolated.
Step 3: Register Strategies
We create a strategy map:
export const strategyMap = { "gpt-4.5": openAIStrategy, "claude-3": claudeStrategy, gemini: geminiStrategy,} as const;
export type ModelType = keyof typeof strategyMap;Step 4: Strategy Selector (Factory)
export function getModelStrategy(model: ModelType) { return strategyMap[model];}Usage
const strategy = getModelStrategy("gpt-4.5");strategy.sendMessage("Hello");Refactored Chat Component
Now our component becomes clean:
import { strategyMap, type ModelType } from "./pattern";
interface Props { model: ModelType;}
export function Chat({ model }: Props) { const strategy = strategyMap[model];
const handleSend = async () => { const response = await strategy.sendMessage("Hello"); console.log(response); };
const handleEdit = async () => { if (strategy.editMessage) { const response = await strategy.editMessage("123", "Updated"); console.log(response); } };
return ( <> <button onClick={handleSend}>Send</button> <button onClick={handleEdit}>Edit</button> </> );}No if/else.
Adding a new model now only requires:
- Creating a new strategy
- Registering it in the map
That’s it.
Strategy Pattern — The React Way (Using Hooks)
In React, logic is often encapsulated inside hooks.
Let’s convert strategies into hooks.
Step 1: Define Hook Interface
export interface ChatStrategyHook { sendMessage(message: string): Promise<string>; editMessage?(messageId: string, newMessage: string): Promise<string>; deleteMessage?(messageId: string): Promise<void>;}Step 2: Create Strategy Hooks
OpenAI Hook
export function useOpenAIStrategy(): ChatStrategyHook { const sendMessage = async (message: string) => { console.log("Calling OpenAI send...", message); return "OpenAI response"; };
const editMessage = async (messageId: string, newMessage: string) => { console.log("Editing message in OpenAI:", messageId, newMessage); return "Edited OpenAI response"; };
const deleteMessage = async (messageId: string) => { console.log("Deleting message in OpenAI:", messageId); };
return { sendMessage, editMessage, deleteMessage };}Claude Hook
export function useClaudeStrategy(): ChatStrategyHook { const sendMessage = async (message: string) => { console.log("Calling Claude send...", message); return "Claude response"; };
const editMessage = async (messageId: string, newMessage: string) => { console.log("Claude editing message:", messageId, newMessage); return "Claude edited response"; };
return { sendMessage, editMessage };}Gemini Hook
export function useGeminiStrategy(): ChatStrategyHook { const sendMessage = async (message: string) => { console.log("Calling Gemini send...", message); return "Gemini response"; };
return { sendMessage };}Step 3: Register Hooks
export const strategyMap = { "gpt-4.5": useOpenAIStrategy, "claude-3": useClaudeStrategy, gemini: useGeminiStrategy,} as const;
export type ModelType = keyof typeof strategyMap;Step 4: Create Hook Factory
export function createModelHook(model: ModelType) { return strategyMap[model];}Usage
const useModel = createModelHook("gpt-4.5");useModel().sendMessage("Hello");Final React Implementation
import { createModelHook, type ModelType } from "./pattern";
interface Props { model: ModelType;}
export function Chat({ model }: Props) { const useModel = createModelHook(model); const strategy = useModel();
const handleSend = async () => { const response = await strategy.sendMessage("Hello"); console.log(response); };
const handleEdit = async () => { if (strategy.editMessage) { const response = await strategy.editMessage("123", "Updated"); console.log(response); } };
return ( <> <button onClick={handleSend}>Send</button> <button onClick={handleEdit}>Edit</button> </> );}Improving Further: Using Context
If multiple components need access to the same model strategy, writing this everywhere:
const useModel = createModelHook(model);const strategy = useModel();can become repetitive.
In that case, you can:
- Create a
ModelContext - Provide the selected strategy at a top level
- Consume it using
useContext
We’ll explore this Context Pattern in the next article.
When Should You Use Strategy Pattern?
When:
- You have multiple interchangeable behaviors
- Logic changes based on runtime input
- Your
if/elseblocks keep growing
Avoid when:
- The app is very small
- Only 1–2 simple conditions exist
Overengineering small apps is unnecessary.
Conclusion
Using the Strategy Pattern in React:
- Removes conditional complexity
- Makes code extensible
- Keeps components clean
- Improves maintainability
- Follows SOLID principles
Instead of modifying existing code, you extend behavior by adding new strategies.
That’s scalable architecture.
Source Code
GitHub Repository: https://github.com/romanpoudel/pnpm-monorepo/tree/main/apps/frontend-react/src/strategy-pattern