Back to CourseLesson 7 of 8

State Management Patterns

AI applications have unique state management needs. This lesson covers patterns for managing conversations, streaming data, and complex multi-agent interactions.

Conversation State

RANA hooks manage conversation state internally, but you may need additional state for complex applications:

import { useChat } from '@rana/react';
import { create } from 'zustand';

// Global conversation store
const useConversationStore = create((set, get) => ({
  conversations: {},
  activeId: null,

  createConversation: () => {
    const id = crypto.randomUUID();
    set(state => ({
      conversations: {
        ...state.conversations,
        [id]: { id, messages: [], createdAt: Date.now() }
      },
      activeId: id
    }));
    return id;
  },

  setActive: (id) => set({ activeId: id }),

  updateMessages: (id, messages) => set(state => ({
    conversations: {
      ...state.conversations,
      [id]: { ...state.conversations[id], messages }
    }
  }))
}));

function ChatWithHistory() {
  const { conversations, activeId, updateMessages } = useConversationStore();
  const activeConvo = conversations[activeId];

  const chat = useChat({
    api: '/api/chat',
    initialMessages: activeConvo?.messages || [],
    onFinish: (message) => {
      // Sync to global store
      updateMessages(activeId, [...chat.messages, message]);
    }
  });

  // ... render
}

Streaming State

Handle streaming data with proper state updates:

import { useState, useCallback } from 'react';
import { useChat } from '@rana/react';

function StreamingChat() {
  // Track streaming state
  const [streamingMessageId, setStreamingMessageId] = useState<string | null>(null);

  const chat = useChat({
    api: '/api/chat',
    onResponse: (response) => {
      // New message started streaming
      const messageId = response.headers.get('x-message-id');
      setStreamingMessageId(messageId);
    },
    onFinish: () => {
      setStreamingMessageId(null);
    }
  });

  return (
    <div>
      {chat.messages.map(m => (
        <Message
          key={m.id}
          message={m}
          isStreaming={m.id === streamingMessageId}
        />
      ))}
    </div>
  );
}

// Animated message component
function Message({ message, isStreaming }) {
  return (
    <div className={`message ${isStreaming ? 'streaming' : ''}`}>
      {message.content}
      {isStreaming && <span className="cursor" />}
    </div>
  );
}

Multi-Agent State

Coordinating state across multiple agents:

import { create } from 'zustand';

interface AgentState {
  agents: Record<string, {
    id: string;
    status: 'idle' | 'thinking' | 'executing' | 'done';
    lastOutput: string | null;
    tools: string[];
  }>;
  pipeline: string[];  // Order of agent execution
  currentStep: number;
}

const useAgentOrchestrator = create<AgentState>((set, get) => ({
  agents: {},
  pipeline: [],
  currentStep: 0,

  registerAgent: (agent) => set(state => ({
    agents: { ...state.agents, [agent.id]: agent }
  })),

  startPipeline: async (input: string) => {
    const { pipeline, agents } = get();

    for (let i = 0; i < pipeline.length; i++) {
      set({ currentStep: i });
      const agentId = pipeline[i];

      // Update agent status
      set(state => ({
        agents: {
          ...state.agents,
          [agentId]: { ...state.agents[agentId], status: 'thinking' }
        }
      }));

      // Execute agent
      const result = await executeAgent(agentId, input);

      // Store result
      set(state => ({
        agents: {
          ...state.agents,
          [agentId]: {
            ...state.agents[agentId],
            status: 'done',
            lastOutput: result
          }
        }
      }));

      // Pass output to next agent
      input = result;
    }
  }
}));

Optimistic Updates

Show immediate feedback while waiting for AI responses:

function OptimisticChat() {
  const [optimisticMessages, setOptimisticMessages] = useState([]);

  const chat = useChat({
    api: '/api/chat',
    onError: (error) => {
      // Remove optimistic message on error
      setOptimisticMessages([]);
    }
  });

  const handleSend = () => {
    // Add optimistic user message
    const optimisticMsg = {
      id: 'temp-' + Date.now(),
      role: 'user',
      content: chat.input,
      isOptimistic: true
    };
    setOptimisticMessages([optimisticMsg]);

    // Actually send
    chat.send();
  };

  // Merge optimistic with real messages
  const allMessages = [...chat.messages, ...optimisticMessages];

  return (
    <div>
      {allMessages.map(m => (
        <div
          key={m.id}
          className={m.isOptimistic ? 'opacity-70' : ''}
        >
          {m.content}
        </div>
      ))}
    </div>
  );
}

Persisting State

Save and restore conversation state:

import { useChat } from '@rana/react';
import { useEffect } from 'react';

function PersistentChat({ conversationId }) {
  // Load from storage
  const loadMessages = () => {
    const saved = localStorage.getItem(`chat-${conversationId}`);
    return saved ? JSON.parse(saved) : [];
  };

  const chat = useChat({
    api: '/api/chat',
    id: conversationId,
    initialMessages: loadMessages(),
    onFinish: (message) => {
      // Save after each message
      localStorage.setItem(
        `chat-${conversationId}`,
        JSON.stringify(chat.messages)
      );
    }
  });

  // Also save on unmount
  useEffect(() => {
    return () => {
      localStorage.setItem(
        `chat-${conversationId}`,
        JSON.stringify(chat.messages)
      );
    };
  }, [conversationId, chat.messages]);

  return <ChatUI chat={chat} />;
}

// For server-side persistence
async function saveConversation(id: string, messages: Message[]) {
  await fetch('/api/conversations', {
    method: 'PUT',
    body: JSON.stringify({ id, messages })
  });
}

Context Management

Use React Context for shared AI state:

import { createContext, useContext, useState } from 'react';

interface AIContextValue {
  model: string;
  setModel: (model: string) => void;
  systemPrompt: string;
  setSystemPrompt: (prompt: string) => void;
  temperature: number;
  setTemperature: (temp: number) => void;
}

const AIContext = createContext<AIContextValue | null>(null);

export function AIProvider({ children }) {
  const [model, setModel] = useState('claude-sonnet-4-20250514');
  const [systemPrompt, setSystemPrompt] = useState('You are helpful.');
  const [temperature, setTemperature] = useState(0.7);

  return (
    <AIContext.Provider value={{
      model, setModel,
      systemPrompt, setSystemPrompt,
      temperature, setTemperature
    }}>
      {children}
    </AIContext.Provider>
  );
}

export function useAI() {
  const context = useContext(AIContext);
  if (!context) throw new Error('useAI must be used within AIProvider');
  return context;
}

// Usage in components
function Chat() {
  const { model, systemPrompt, temperature } = useAI();

  const chat = useChat({
    api: '/api/chat',
    body: { model, systemPrompt, temperature }
  });

  // ...
}

Derived State

Compute values from conversation state:

import { useMemo } from 'react';
import { useChat } from '@rana/react';

function ChatAnalytics() {
  const chat = useChat({ api: '/api/chat' });

  // Derive analytics from messages
  const analytics = useMemo(() => ({
    totalMessages: chat.messages.length,
    userMessages: chat.messages.filter(m => m.role === 'user').length,
    assistantMessages: chat.messages.filter(m => m.role === 'assistant').length,
    totalTokens: chat.messages.reduce((acc, m) => acc + (m.tokens || 0), 0),
    averageResponseLength: chat.messages
      .filter(m => m.role === 'assistant')
      .reduce((acc, m, _, arr) => acc + m.content.length / arr.length, 0)
  }), [chat.messages]);

  return (
    <div>
      <div className="analytics">
        <span>{analytics.totalMessages} messages</span>
        <span>{analytics.totalTokens} tokens</span>
      </div>
      <ChatUI chat={chat} />
    </div>
  );
}

Best Practices

  • Keep UI state separate from AI state - Don't mix loading spinners with message content
  • Use derived state for computed values - Don't duplicate data that can be calculated
  • Persist strategically - Save to localStorage for quick access, sync to server for durability
  • Handle race conditions - Use IDs to match responses to requests when sending multiple messages
  • Clean up old conversations - Implement retention policies to prevent unbounded storage growth

What's Next?

In the final lesson of this course, we'll build your first complete AI agent that combines everything you've learned.