Claude Code Integration with Modern State Management
This guide explores patterns and best practices for integrating Claude Code with modern state management libraries in React applications. As AI-assisted development becomes more prevalent, managing state across AI interactions, user inputs, and application logic requires careful consideration.
Overview
When building applications with Claude Code, you’ll often need to:
- Track AI conversation state and history
- Manage loading states during AI operations
- Handle errors and retries gracefully
- Synchronize AI-generated content with application state
- Maintain state consistency across multiple AI sessions
State Management Library Comparison
Redux Toolkit
Redux Toolkit (RTK) is ideal for Claude Code integrations that require:
- Complex middleware for AI request handling
- Time-travel debugging for AI conversations
- Strict patterns for team collaboration
- Advanced caching strategies for AI responses
Zustand
Zustand excels in Claude Code integrations with:
- Minimal boilerplate for quick prototypes
- Lightweight state management (~8KB)
- Direct store access without providers
- Simple async operation handling
Jotai
Jotai works well for:
- Atomic state management for AI features
- Complex interdependent AI states
- Fine-grained reactivity
- Component-level state isolation
Implementation Patterns
1. Redux Toolkit + Claude Code
// store/claudeSlice.ts
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { ClaudeCode } from '@anthropic-ai/claude-code';
interface ClaudeState {
messages: Message[];
isLoading: boolean;
error: string | null;
sessionId: string;
context: any[];
}
// Async thunk for Claude queries
export const sendClaudeQuery = createAsyncThunk(
'claude/sendQuery',
async ({ prompt, options }: { prompt: string; options?: any }) => {
const claude = new ClaudeCode({
apiKey: process.env.CLAUDE_API_KEY,
model: 'claude-3-sonnet'
});
const response = await claude.query(prompt, options);
return response;
}
);
const claudeSlice = createSlice({
name: 'claude',
initialState: {
messages: [],
isLoading: false,
error: null,
sessionId: '',
context: []
} as ClaudeState,
reducers: {
addMessage: (state, action) => {
state.messages.push(action.payload);
},
clearSession: (state) => {
state.messages = [];
state.context = [];
},
updateContext: (state, action) => {
state.context = action.payload;
}
},
extraReducers: (builder) => {
builder
.addCase(sendClaudeQuery.pending, (state) => {
state.isLoading = true;
state.error = null;
})
.addCase(sendClaudeQuery.fulfilled, (state, action) => {
state.isLoading = false;
state.messages.push({
role: 'assistant',
content: action.payload.content,
timestamp: new Date().toISOString()
});
})
.addCase(sendClaudeQuery.rejected, (state, action) => {
state.isLoading = false;
state.error = action.error.message || 'Failed to get response';
});
}
});
export const { addMessage, clearSession, updateContext } = claudeSlice.actions;
export default claudeSlice.reducer;2. Zustand + Claude Code
// stores/claudeStore.ts
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import { ClaudeCode } from '@anthropic-ai/claude-code';
interface Message {
role: 'user' | 'assistant';
content: string;
timestamp: string;
toolCalls?: any[];
}
interface ClaudeStore {
// State
messages: Message[];
isLoading: boolean;
error: string | null;
streamingContent: string;
// Actions
sendMessage: (prompt: string) => Promise<void>;
clearChat: () => void;
setStreamingContent: (content: string) => void;
}
const claude = new ClaudeCode({
apiKey: process.env.NEXT_PUBLIC_CLAUDE_API_KEY!,
model: 'claude-3-sonnet'
});
export const useClaudeStore = create<ClaudeStore>()(
devtools(
persist(
(set, get) => ({
messages: [],
isLoading: false,
error: null,
streamingContent: '',
sendMessage: async (prompt: string) => {
// Add user message
set((state) => ({
messages: [...state.messages, {
role: 'user',
content: prompt,
timestamp: new Date().toISOString()
}],
isLoading: true,
error: null,
streamingContent: ''
}));
try {
// Stream response
const response = await claude
.onMessage((msg) => {
if (msg.type === 'content') {
set((state) => ({
streamingContent: state.streamingContent + msg.text
}));
}
})
.query(prompt)
.stream();
// Add complete response
set((state) => ({
messages: [...state.messages, {
role: 'assistant',
content: state.streamingContent,
timestamp: new Date().toISOString()
}],
isLoading: false,
streamingContent: ''
}));
} catch (error) {
set({
isLoading: false,
error: error instanceof Error ? error.message : 'Unknown error'
});
}
},
clearChat: () => set({ messages: [], error: null }),
setStreamingContent: (content) => set({ streamingContent: content })
}),
{
name: 'claude-chat-storage',
partialize: (state) => ({ messages: state.messages })
}
)
)
);3. React Hooks for Claude Code
// hooks/useClaude.ts
import { useState, useCallback, useRef } from 'react';
import { ClaudeCode } from '@anthropic-ai/claude-code';
interface UseClaudeOptions {
model?: string;
onStream?: (content: string) => void;
onToolUse?: (tool: any) => void;
}
export function useClaude(options: UseClaudeOptions = {}) {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [messages, setMessages] = useState<any[]>([]);
const claudeRef = useRef<ClaudeCode>();
// Initialize Claude instance
if (!claudeRef.current) {
claudeRef.current = new ClaudeCode({
apiKey: process.env.NEXT_PUBLIC_CLAUDE_API_KEY!,
model: options.model || 'claude-3-sonnet'
});
}
const sendMessage = useCallback(async (prompt: string, context?: any[]) => {
setIsLoading(true);
setError(null);
try {
const claude = claudeRef.current!;
// Configure callbacks
if (options.onStream) {
claude.onMessage((msg) => {
if (msg.type === 'content') {
options.onStream!(msg.text);
}
});
}
if (options.onToolUse) {
claude.onToolUse(options.onToolUse);
}
// Send query with context
const response = await claude.query(prompt, {
context,
tools: ['Read', 'Write', 'Bash']
});
setMessages(prev => [...prev,
{ role: 'user', content: prompt },
{ role: 'assistant', content: response.content }
]);
return response;
} catch (err) {
setError(err as Error);
throw err;
} finally {
setIsLoading(false);
}
}, [options]);
const clearMessages = useCallback(() => {
setMessages([]);
}, []);
return {
sendMessage,
clearMessages,
messages,
isLoading,
error
};
}Advanced Patterns
Session Management with Multiple Stores
// stores/sessionManager.ts
import { create } from 'zustand';
import { ClaudeCode } from '@anthropic-ai/claude-code';
interface Session {
id: string;
name: string;
claude: ClaudeCode;
messages: any[];
context: any[];
}
interface SessionStore {
sessions: Map<string, Session>;
activeSessionId: string | null;
createSession: (name: string) => string;
switchSession: (id: string) => void;
sendToSession: (sessionId: string, prompt: string) => Promise<any>;
deleteSession: (id: string) => void;
}
export const useSessionStore = create<SessionStore>((set, get) => ({
sessions: new Map(),
activeSessionId: null,
createSession: (name: string) => {
const id = crypto.randomUUID();
const claude = new ClaudeCode({
apiKey: process.env.NEXT_PUBLIC_CLAUDE_API_KEY!
});
set((state) => {
const newSessions = new Map(state.sessions);
newSessions.set(id, {
id,
name,
claude,
messages: [],
context: []
});
return { sessions: newSessions, activeSessionId: id };
});
return id;
},
switchSession: (id: string) => {
set({ activeSessionId: id });
},
sendToSession: async (sessionId: string, prompt: string) => {
const session = get().sessions.get(sessionId);
if (!session) throw new Error('Session not found');
const response = await session.claude.query(prompt, {
context: session.context
});
set((state) => {
const updatedSessions = new Map(state.sessions);
const session = updatedSessions.get(sessionId)!;
session.messages.push(
{ role: 'user', content: prompt },
{ role: 'assistant', content: response.content }
);
return { sessions: updatedSessions };
});
return response;
},
deleteSession: (id: string) => {
set((state) => {
const newSessions = new Map(state.sessions);
newSessions.delete(id);
return {
sessions: newSessions,
activeSessionId: state.activeSessionId === id ? null : state.activeSessionId
};
});
}
}));Error Handling and Retry Logic
// utils/claudeRetry.ts
import { ClaudeCode } from '@anthropic-ai/claude-code';
interface RetryOptions {
maxRetries?: number;
backoffMs?: number;
onRetry?: (attempt: number, error: Error) => void;
}
export async function queryWithRetry(
claude: ClaudeCode,
prompt: string,
options: RetryOptions = {}
) {
const { maxRetries = 3, backoffMs = 1000, onRetry } = options;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await claude.query(prompt);
} catch (error) {
if (attempt === maxRetries) throw error;
if (onRetry) {
onRetry(attempt, error as Error);
}
// Exponential backoff
await new Promise(resolve =>
setTimeout(resolve, backoffMs * Math.pow(2, attempt - 1))
);
}
}
}
// Usage in Zustand store
export const useClaudeStoreWithRetry = create<any>((set, get) => ({
sendMessageWithRetry: async (prompt: string) => {
set({ isLoading: true, error: null });
try {
const response = await queryWithRetry(claude, prompt, {
onRetry: (attempt, error) => {
set({
retryStatus: `Retry attempt ${attempt}/3: ${error.message}`
});
}
});
// Handle success
set((state) => ({
messages: [...state.messages, response],
isLoading: false,
retryStatus: null
}));
} catch (error) {
set({
isLoading: false,
error: error instanceof Error ? error.message : 'Unknown error'
});
}
}
}));Best Practices
1. State Structure
- Keep AI conversation state separate from UI state
- Store raw responses and derived data separately
- Implement proper error boundaries for AI failures
- Use optimistic updates for better UX
2. Performance Optimization
- Implement response caching for repeated queries
- Use debouncing for user input before AI calls
- Stream responses for better perceived performance
- Lazy load conversation history
3. Security Considerations
- Never store API keys in client-side state
- Sanitize AI responses before rendering
- Implement rate limiting on the client
- Use environment variables for configuration
4. Testing Strategies
// __tests__/claudeStore.test.ts
import { renderHook, act } from '@testing-library/react';
import { useClaudeStore } from '../stores/claudeStore';
// Mock Claude Code SDK
jest.mock('@anthropic-ai/claude-code', () => ({
ClaudeCode: jest.fn().mockImplementation(() => ({
query: jest.fn().mockResolvedValue({ content: 'Mocked response' }),
onMessage: jest.fn().mockReturnThis(),
stream: jest.fn().mockResolvedValue({})
}))
}));
describe('Claude Store', () => {
it('should send message and update state', async () => {
const { result } = renderHook(() => useClaudeStore());
await act(async () => {
await result.current.sendMessage('Test prompt');
});
expect(result.current.messages).toHaveLength(2);
expect(result.current.messages[0].role).toBe('user');
expect(result.current.messages[1].role).toBe('assistant');
});
});Conclusion
Integrating Claude Code with modern state management requires careful consideration of async operations, error handling, and user experience. Whether you choose Redux Toolkit for its robust ecosystem or Zustand for its simplicity, the patterns shown here provide a foundation for building AI-powered React applications.