Overview
Stream forwarding lets agents control any component in your application. When an agent generates content, you can forward it to code editors, preview panels, file trees, or any custom component - making agents truly interactive with your UI.
The Power of Forwarding
Instead of just showing agent responses in chat, you can:
- Update code editors as agents write code
- Populate forms with generated data
- Render previews of created content
- Update databases with structured data
- Control UI state based on agent actions
Basic Forwarding
Single Action
Forward agent output to your component:
import { useForwarding } from 'react-chat-agent';
function CodeEditor() {
const [code, setCode] = useState('');
useForwarding('code_generation', (incrementalText, fullText) => {
setCode(fullText); // Update editor with agent's code
}, {
mode: 'replace', // Don't show in chat, only in editor
animated: false // Instant updates
});
return <MonacoEditor value={code} />;
}Multiple Actions
Control different parts of your app:
function DevEnvironment() {
const [code, setCode] = useState('');
const [readme, setReadme] = useState('');
const [tests, setTests] = useState('');
useForwarding({
write_code: {
handler: (inc, full) => setCode(full),
mode: 'replace'
},
write_readme: {
handler: (inc, full) => setReadme(full),
mode: 'replace'
},
write_tests: {
handler: (inc, full) => setTests(full),
mode: 'replace'
}
});
return (
<div className="grid grid-cols-3 gap-4">
<Editor value={code} language="typescript" />
<MarkdownPreview value={readme} />
<Editor value={tests} language="typescript" />
</div>
);
}Forwarding Modes
Replace Mode
Agent output goes ONLY to your component, not chat:
useForwarding('generate_report', handler, {
mode: 'replace' // ← Hide from chat, show only where you want
});Use case: Code editors, form fields, databases - anywhere you want full control.
Dual Mode
Agent output appears in BOTH chat and your component:
useForwarding('explain_code', handler, {
mode: 'dual' // ← Show in chat AND your component
});Use case: When you want users to see the conversation while also updating your UI.
Real-World Examples
AI Code Editor
Agent writes code directly into your Monaco editor:
function AICodeEditor() {
const editorRef = useRef();
const [language, setLanguage] = useState('javascript');
useForwarding('write_code', (incremental, full, { event }) => {
if (editorRef.current) {
editorRef.current.setValue(full);
}
}, {
mode: 'replace',
animated: false
});
return (
<div>
<select onChange={(e) => setLanguage(e.value)}>
<option>javascript</option>
<option>python</option>
<option>rust</option>
</select>
<MonacoEditor
ref={editorRef}
language={language}
theme="vs-dark"
/>
</div>
);
}Live Documentation Generator
Agent creates docs that render in real-time:
function LiveDocsGenerator() {
const [markdown, setMarkdown] = useState('');
useForwarding('generate_docs', (inc, full) => {
setMarkdown(full);
}, {
mode: 'replace',
animated: true // Typing effect
});
return (
<div className="grid grid-cols-2 gap-4">
<div className="prose">
<ReactMarkdown>{markdown}</ReactMarkdown>
</div>
<div className="preview">
<ComponentPreview code={markdown} />
</div>
</div>
);
}Form Auto-Fill
Agent fills forms with structured data:
function SmartForm() {
const [formData, setFormData] = useState({});
useForwarding('extract_data', (inc, full, { event }) => {
if (event === 'end') {
try {
const data = JSON.parse(full);
setFormData(data);
} catch (e) {
console.error('Invalid JSON');
}
}
}, {
mode: 'replace'
});
return (
<form>
<input value={formData.name || ''} />
<input value={formData.email || ''} />
<input value={formData.company || ''} />
</form>
);
}Multi-File Editor
Agent creates entire project structures:
function ProjectEditor() {
const [files, setFiles] = useState({});
const [activeFile, setActiveFile] = useState('');
useForwarding({
create_component: {
handler: (inc, full, { event }) => {
if (event === 'end') {
setFiles(prev => ({
...prev,
'Component.tsx': full
}));
}
},
mode: 'replace'
},
create_styles: {
handler: (inc, full, { event }) => {
if (event === 'end') {
setFiles(prev => ({
...prev,
'styles.css': full
}));
}
},
mode: 'replace'
},
create_test: {
handler: (inc, full, { event }) => {
if (event === 'end') {
setFiles(prev => ({
...prev,
'Component.test.tsx': full
}));
}
},
mode: 'replace'
}
});
return (
<div className="flex h-screen">
<FileTree files={files} onSelect={setActiveFile} />
<Editor file={files[activeFile]} />
</div>
);
}Handler Function
Parameters
handler(incrementalText, fullText, metadata)- incrementalText: New text since last chunk (for appending)
- fullText: Complete text from start (for replacing)
- metadata:
{ action, isLive, event: 'chunk' | 'end' }
Usage Patterns
Append mode (for live updates):
handler: (inc, full) => {
setText(prev => prev + inc); // Append new text
}Replace mode (for final value):
handler: (inc, full, { event }) => {
if (event === 'end') {
setText(full); // Set complete text once done
}
}Mixed mode (live updates + final processing):
handler: (inc, full, { event }) => {
setPreview(full); // Live preview
if (event === 'end') {
saveToDatabase(full); // Final save
generatePDF(full);
notify('Generation complete');
}
}Provider-Level Forwarding
Set up global handlers in ChatProvider:
<ChatProvider
authConfig={authConfig}
forwardingHandlers={{
code_generation: {
handler: updateCodeEditor,
mode: 'replace',
animated: false
},
design_generation: {
handler: updateDesignCanvas,
mode: 'replace'
}
}}
>
<App />
</ChatProvider>Dynamic Registration
Register/unregister handlers on the fly:
function DynamicHandler() {
const forwarding = useForwarding();
const [enabled, setEnabled] = useState(false);
useEffect(() => {
if (enabled) {
return forwarding.register('custom_action', handler, {
mode: 'replace'
});
}
}, [enabled]);
return (
<button onClick={() => setEnabled(!enabled)}>
{enabled ? 'Disable' : 'Enable'} Handler
</button>
);
}Animation Control
Instant Updates
For code editors and data:
useForwarding('write_code', handler, {
animated: false // No typing animation
});Typing Effect
For user-facing text:
useForwarding('explain', handler, {
animated: true // Smooth typing effect
});Advanced Patterns
Conditional Forwarding
Forward based on conditions:
function ConditionalForwarding() {
const [mode, setMode] = useState('preview');
useForwarding('generate_content', (inc, full) => {
if (mode === 'preview') {
updatePreview(full);
} else if (mode === 'edit') {
updateEditor(full);
}
});
}Multi-Target Forwarding
Send same content to multiple components:
useForwarding('generate_article', (inc, full) => {
updateEditor(full);
updatePreview(full);
updateWordCount(full.split(' ').length);
updateReadTime(calculateReadTime(full));
});Transformation Pipeline
Transform data before forwarding:
useForwarding('generate_json', (inc, full, { event }) => {
if (event === 'end') {
try {
const parsed = JSON.parse(full);
const transformed = transformData(parsed);
setData(transformed);
} catch (e) {
setError('Invalid JSON');
}
}
});Best Practices
Performance
// ✅ Use incremental for appending
handler: (inc) => setText(prev => prev + inc)
// ❌ Don't replace entire text each chunk
handler: (inc, full) => setText(full) // Causes re-rendersMemory Management
// ✅ Clean up on event end
handler: (inc, full, { event }) => {
updatePreview(full);
if (event === 'end') {
clearTempData();
optimizeContent(full);
}
}Error Handling
handler: (inc, full, { event }) => {
try {
const data = processContent(full);
updateUI(data);
} catch (error) {
console.error('Forwarding error:', error);
showError('Failed to process content');
}
}TypeScript Types
import type {
ForwardingHandler,
ForwardingHandlerConfig,
ForwardingMetadata
} from 'react-chat-agent';
const handler: ForwardingHandler = (inc, full, meta) => {
// Type-safe handler
};
const config: ForwardingHandlerConfig = {
handler,
mode: 'replace',
animated: false
};Next Steps
- Status Events - Monitor agent actions
- Multi-Instance - Run multiple agents
- Customization - Style your components