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.