Skip to content

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:

  1. Define a common interface
  2. Create separate strategies
  3. Register strategies (mapping)
  4. 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/else blocks 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