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.

See Also

External Resources