React Integration Guide¶
This guide shows you how to integrate AgentFlow React into your React applications with best practices, custom hooks, and common patterns.
๐ฆ Installation¶
๐ฏ Core Concepts¶
Client Initialization¶
The AgentFlowClient should be initialized once and shared across your application.
โ Don't create new clients in every component:
function MyComponent() {
// DON'T DO THIS - creates new client on every render
const client = new AgentFlowClient({ baseUrl: 'http://localhost:8000' });
// ...
}
โ Do create client once and reuse:
// Option 1: Module-level singleton
// utils/agentflow.ts
export const agentFlowClient = new AgentFlowClient({
baseUrl: process.env.REACT_APP_AGENTFLOW_URL || 'http://localhost:8000'
});
// MyComponent.tsx
import { agentFlowClient } from './utils/agentflow';
๐๏ธ Context Provider Pattern¶
The recommended approach is to use React Context to provide the client throughout your app.
Step 1: Create AgentFlow Context¶
// contexts/AgentFlowContext.tsx
import React, { createContext, useContext, ReactNode } from 'react';
import { AgentFlowClient } from 'agentflow-react';
interface AgentFlowContextType {
client: AgentFlowClient;
}
const AgentFlowContext = createContext<AgentFlowContextType | undefined>(undefined);
interface AgentFlowProviderProps {
children: ReactNode;
baseUrl: string;
authToken?: string;
debug?: boolean;
}
export function AgentFlowProvider({
children,
baseUrl,
authToken,
debug = false
}: AgentFlowProviderProps) {
// Create client once
const client = React.useMemo(
() => new AgentFlowClient({ baseUrl, authToken, debug }),
[baseUrl, authToken, debug]
);
return (
<AgentFlowContext.Provider value={{ client }}>
{children}
</AgentFlowContext.Provider>
);
}
export function useAgentFlow() {
const context = useContext(AgentFlowContext);
if (!context) {
throw new Error('useAgentFlow must be used within AgentFlowProvider');
}
return context.client;
}
Step 2: Wrap Your App¶
// App.tsx
import { AgentFlowProvider } from './contexts/AgentFlowContext';
function App() {
return (
<AgentFlowProvider
baseUrl={process.env.REACT_APP_AGENTFLOW_URL || 'http://localhost:8000'}
authToken={process.env.REACT_APP_AUTH_TOKEN}
debug={process.env.NODE_ENV === 'development'}
>
<YourApp />
</AgentFlowProvider>
);
}
Step 3: Use in Components¶
// components/Chat.tsx
import { useAgentFlow } from '../contexts/AgentFlowContext';
function Chat() {
const client = useAgentFlow();
const sendMessage = async (text: string) => {
const result = await client.invoke([/* ... */]);
// ...
};
return <div>{/* ... */}</div>;
}
๐ช Custom Hooks¶
useInvoke Hook¶
Manage invoke requests with loading and error states:
// hooks/useInvoke.ts
import { useState } from 'react';
import { Message, InvokeResult } from 'agentflow-react';
import { useAgentFlow } from '../contexts/AgentFlowContext';
interface UseInvokeOptions {
recursion_limit?: number;
response_granularity?: 'full' | 'partial' | 'low';
}
export function useInvoke(options: UseInvokeOptions = {}) {
const client = useAgentFlow();
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [result, setResult] = useState<InvokeResult | null>(null);
const invoke = async (messages: Message[]) => {
setLoading(true);
setError(null);
try {
const response = await client.invoke(messages, options);
setResult(response);
return response;
} catch (err) {
const error = err instanceof Error ? err : new Error('Unknown error');
setError(error);
throw error;
} finally {
setLoading(false);
}
};
const reset = () => {
setResult(null);
setError(null);
setLoading(false);
};
return {
invoke,
loading,
error,
result,
reset
};
}
Usage:
function ChatComponent() {
const { invoke, loading, error, result } = useInvoke({
recursion_limit: 10,
response_granularity: 'low'
});
const sendMessage = async (text: string) => {
try {
await invoke([Message.text_message(text, 'user')]);
} catch (err) {
console.error('Failed to send message:', err);
}
};
return (
<div>
{loading && <div>Loading...</div>}
{error && <div>Error: {error.message}</div>}
{result && <div>{/* Display messages */}</div>}
</div>
);
}
useStream Hook¶
Handle streaming responses with real-time updates:
// hooks/useStream.ts
import { useState, useCallback } from 'react';
import { Message, StreamChunk } from 'agentflow-react';
import { useAgentFlow } from '../contexts/AgentFlowContext';
interface UseStreamOptions {
onChunk?: (chunk: StreamChunk) => void;
onError?: (error: Error) => void;
onComplete?: () => void;
}
export function useStream(options: UseStreamOptions = {}) {
const client = useAgentFlow();
const [streaming, setStreaming] = useState(false);
const [chunks, setChunks] = useState<StreamChunk[]>([]);
const [error, setError] = useState<Error | null>(null);
const startStream = useCallback(async (messages: Message[]) => {
setStreaming(true);
setError(null);
setChunks([]);
try {
const stream = client.stream(messages, {
response_granularity: 'low'
});
for await (const chunk of stream) {
setChunks(prev => [...prev, chunk]);
options.onChunk?.(chunk);
}
options.onComplete?.();
} catch (err) {
const error = err instanceof Error ? err : new Error('Stream error');
setError(error);
options.onError?.(error);
} finally {
setStreaming(false);
}
}, [client, options]);
const reset = useCallback(() => {
setChunks([]);
setError(null);
setStreaming(false);
}, []);
return {
startStream,
streaming,
chunks,
error,
reset
};
}
Usage:
function StreamingChat() {
const { startStream, streaming, chunks, error } = useStream({
onChunk: (chunk) => console.log('New chunk:', chunk),
onComplete: () => console.log('Stream complete')
});
const sendMessage = (text: string) => {
startStream([Message.text_message(text, 'user')]);
};
const messages = chunks
.filter(chunk => chunk.event === 'message')
.map(chunk => chunk.message)
.filter(Boolean);
return (
<div>
{messages.map((msg, i) => (
<div key={i}>{msg.content}</div>
))}
{streaming && <div>Streaming...</div>}
{error && <div>Error: {error.message}</div>}
</div>
);
}
useStateSchema Hook¶
Fetch and cache state schema for form generation:
// hooks/useStateSchema.ts
import { useState, useEffect } from 'react';
import { AgentStateSchema } from 'agentflow-react';
import { useAgentFlow } from '../contexts/AgentFlowContext';
export function useStateSchema() {
const client = useAgentFlow();
const [schema, setSchema] = useState<AgentStateSchema | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
let mounted = true;
const fetchSchema = async () => {
try {
const response = await client.graphStateSchema();
if (mounted) {
setSchema(response.data);
}
} catch (err) {
if (mounted) {
setError(err instanceof Error ? err : new Error('Failed to fetch schema'));
}
} finally {
if (mounted) {
setLoading(false);
}
}
};
fetchSchema();
return () => {
mounted = false;
};
}, [client]);
return { schema, loading, error };
}
Usage:
function DynamicForm() {
const { schema, loading, error } = useStateSchema();
if (loading) return <div>Loading schema...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!schema) return null;
return (
<form>
{Object.entries(schema.properties).map(([name, field]) => (
<div key={name}>
<label>{field.description || name}</label>
<input
type={field.type === 'number' ? 'number' : 'text'}
defaultValue={field.default}
/>
</div>
))}
</form>
);
}
useMessages Hook¶
Manage conversation message history:
// hooks/useMessages.ts
import { useState, useCallback } from 'react';
import { Message } from 'agentflow-react';
export function useMessages(initialMessages: Message[] = []) {
const [messages, setMessages] = useState<Message[]>(initialMessages);
const addMessage = useCallback((message: Message) => {
setMessages(prev => [...prev, message]);
}, []);
const addMessages = useCallback((newMessages: Message[]) => {
setMessages(prev => [...prev, ...newMessages]);
}, []);
const replaceMessages = useCallback((newMessages: Message[]) => {
setMessages(newMessages);
}, []);
const clearMessages = useCallback(() => {
setMessages([]);
}, []);
const updateLastMessage = useCallback((updater: (msg: Message) => Message) => {
setMessages(prev => {
if (prev.length === 0) return prev;
return [...prev.slice(0, -1), updater(prev[prev.length - 1])];
});
}, []);
return {
messages,
addMessage,
addMessages,
replaceMessages,
clearMessages,
updateLastMessage
};
}
Usage:
function Chat() {
const { messages, addMessage, replaceMessages } = useMessages();
const { invoke } = useInvoke();
const sendMessage = async (text: string) => {
const userMsg = Message.text_message(text, 'user');
addMessage(userMsg);
const result = await invoke([...messages, userMsg]);
replaceMessages(result.messages);
};
return <div>{/* Render messages */}</div>;
}
๐จ Component Patterns¶
Loading States¶
function Chat() {
const { invoke, loading } = useInvoke();
return (
<div>
{loading && (
<div className="loading-indicator">
<span>Thinking...</span>
<div className="spinner"></div>
</div>
)}
</div>
);
}
Error Handling¶
function Chat() {
const { invoke, error } = useInvoke();
const [showError, setShowError] = useState(false);
useEffect(() => {
if (error) {
setShowError(true);
setTimeout(() => setShowError(false), 5000);
}
}, [error]);
return (
<div>
{showError && (
<div className="error-banner">
<span>Error: {error?.message}</span>
<button onClick={() => setShowError(false)}>ร</button>
</div>
)}
</div>
);
}
Streaming with Visual Feedback¶
function StreamingMessage({ chunk }: { chunk: StreamChunk }) {
const [isNew, setIsNew] = useState(true);
useEffect(() => {
const timer = setTimeout(() => setIsNew(false), 300);
return () => clearTimeout(timer);
}, []);
return (
<div className={isNew ? 'message fade-in' : 'message'}>
{chunk.message?.content}
</div>
);
}
Tool Execution Indicator¶
Show when the agent is executing remote tools (client-side only).
โ ๏ธ Note: This only applies to remote tools registered client-side. Backend tools (defined in Python) execute on the server and aren't visible here.
function Chat() {
const { messages } = useMessages();
const [executingTools, setExecutingTools] = useState(false);
useEffect(() => {
// Check if last message contains tool calls
const lastMsg = messages[messages.length - 1];
const hasToolCalls = lastMsg?.content?.some(
(block: any) => block.type === 'remote_tool_call'
);
setExecutingTools(hasToolCalls || false);
}, [messages]);
return (
<div>
{executingTools && (
<div className="tool-indicator">
๐ง Executing tools...
</div>
)}
</div>
);
}
๐ Authentication¶
Token from Environment¶
// AgentFlowProvider with env token
<AgentFlowProvider
baseUrl={process.env.REACT_APP_AGENTFLOW_URL!}
authToken={process.env.REACT_APP_AUTH_TOKEN}
>
<App />
</AgentFlowProvider>
Token from Auth Hook¶
function App() {
const { token } = useAuth(); // Your auth hook
return (
<AgentFlowProvider
baseUrl="http://localhost:8000"
authToken={token}
>
<YourApp />
</AgentFlowProvider>
);
}
Dynamic Token Updates¶
// Context with token updates
export function AgentFlowProvider({ children }: { children: ReactNode }) {
const { token } = useAuth();
const client = useMemo(() => {
return new AgentFlowClient({
baseUrl: 'http://localhost:8000',
authToken: token
});
}, [token]); // Recreate client when token changes
return (
<AgentFlowContext.Provider value={{ client }}>
{children}
</AgentFlowContext.Provider>
);
}
๐งช Testing¶
Mock Client for Tests¶
// __mocks__/agentflow-react.ts
export class AgentFlowClient {
async invoke(messages: any[]) {
return {
messages: [
{ role: 'user', content: messages[0].content },
{ role: 'assistant', content: 'Mocked response' }
],
iterations: 1,
recursion_limit_reached: false
};
}
async *stream(messages: any[]) {
yield {
event: 'message',
message: { role: 'assistant', content: 'Mocked stream' }
};
}
registerTool() {}
}
Test with React Testing Library¶
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { AgentFlowProvider } from '../contexts/AgentFlowContext';
import Chat from '../components/Chat';
jest.mock('agentflow-react');
test('sends message and displays response', async () => {
render(
<AgentFlowProvider baseUrl="http://test">
<Chat />
</AgentFlowProvider>
);
const input = screen.getByRole('textbox');
const button = screen.getByRole('button', { name: /send/i });
fireEvent.change(input, { target: { value: 'Hello' } });
fireEvent.click(button);
await waitFor(() => {
expect(screen.getByText('Mocked response')).toBeInTheDocument();
});
});
๐ State Management¶
With Redux¶
// store/agentflowSlice.ts
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { agentFlowClient } from '../utils/agentflow';
export const sendMessage = createAsyncThunk(
'agentflow/sendMessage',
async (messages: Message[]) => {
const response = await agentFlowClient.invoke(messages);
return response;
}
);
const agentflowSlice = createSlice({
name: 'agentflow',
initialState: { messages: [], loading: false, error: null },
reducers: {},
extraReducers: (builder) => {
builder
.addCase(sendMessage.pending, (state) => {
state.loading = true;
})
.addCase(sendMessage.fulfilled, (state, action) => {
state.messages = action.payload.messages;
state.loading = false;
})
.addCase(sendMessage.rejected, (state, action) => {
state.error = action.error.message;
state.loading = false;
});
}
});
With Zustand¶
// store/agentflowStore.ts
import create from 'zustand';
import { Message } from 'agentflow-react';
import { agentFlowClient } from '../utils/agentflow';
interface AgentFlowStore {
messages: Message[];
loading: boolean;
sendMessage: (text: string) => Promise<void>;
}
export const useAgentFlowStore = create<AgentFlowStore>((set, get) => ({
messages: [],
loading: false,
sendMessage: async (text: string) => {
set({ loading: true });
const userMsg = Message.text_message(text, 'user');
const currentMessages = [...get().messages, userMsg];
try {
const result = await agentFlowClient.invoke(currentMessages);
set({ messages: result.messages, loading: false });
} catch (error) {
console.error(error);
set({ loading: false });
}
}
}));
๐ฏ Best Practices¶
โ Do's¶
- Use Context Provider - Share client across app
- Memoize Client - Avoid recreating on every render
- Handle Loading States - Show feedback during requests
- Handle Errors - Display user-friendly error messages
- Type Everything - Use TypeScript for better DX
- Clean Up Effects - Prevent memory leaks with cleanup
- Use Custom Hooks - Encapsulate common patterns
- Test Components - Mock client for unit tests
โ Don'ts¶
- Don't Create Multiple Clients - One per app
- Don't Ignore Errors - Always handle failures
- Don't Block UI - Use loading states
- Don't Store Client in State - Use context or memo
- Don't Forget Cleanup - Cancel pending requests
- Don't Hard-code URLs - Use environment variables
๐ Next Steps¶
- React Examples - Complete component examples
- API Reference - Full API documentation
- Troubleshooting - Common issues
Need more examples? Check out the React Examples guide for complete working components!