React Integration
How to use flow-state.dev's React bindings to build interactive UIs.
FlowProvider
Wrap your app (or a section of it) with FlowProvider to set defaults and register renderers:
import { FlowProvider } from "@flow-state-dev/react";
function App() {
return (
<FlowProvider
flowKind="my-app"
userId="devuser"
renderers={{
message: MessageComponent,
reasoning: ReasoningComponent,
component: {
"chart": ChartComponent,
"data-table": DataTableComponent,
},
}}
>
<MyUI />
</FlowProvider>
);
}
Renderer Registry
Renderers map item types to React components:
- Class-based types (
message,reasoning, etc.) — One component each - Parameterized types (
component,container) — Sub-key lookup byitem.component
Nested providers merge renderers (child keys override parent keys).
Hooks
useFlow — Session Lifecycle
const flow = useFlow({ autoCreateSession: true });
// Available:
flow.sessions; // Session list
flow.activeSessionId; // Current session ID
flow.createSession(); // Create new session
flow.selectSession(id); // Switch session
useSession — Primary Hook
const session = useSession(flow.activeSessionId, {
items: { visibility: "ui" },
});
// Available:
session.detail; // SessionDetail object
session.latestRequest; // Most recent request (any status), or null
session.items; // All items, including sub-agent items
session.messages; // Message items only
session.blockOutputs; // Block output items only
session.isStreaming; // Active request in progress
session.isLoading; // Initial load in progress
session.error; // Error state
// Identity-based filtering:
session.getItemsByAgent("researcher"); // items stamped with agentName
session.getItemsByAgentType("sub"); // items stamped with agentType
// Container-scoped items:
session.getOwnedItems(containerBlockInstanceId);
// Actions:
await session.sendAction("chat", { message: "Hello!" });
await session.abortRequest(); // Stop the in-flight request
await session.resumeLatestRequest(); // Re-run the latest request if it was interrupted/failed
session.refresh(); // Force refetch
Resuming an interrupted request
When a request dies before it can finish — server crash, HMR reload mid-flow, network drop — its record stays as interrupted. latestRequest exposes that, and resumeLatestRequest re-dispatches the same action with the original input, returning a new request id and attaching the stream.
{session.latestRequest?.status === "interrupted" && !session.isStreaming && (
<button onClick={() => session.resumeLatestRequest()}>
Resume
</button>
)}
resumeLatestRequest is a no-op when the latest request is completed, aborted, or in_progress — only interrupted and failed are retryable.
useClientData — Client Data
const clientData = useClientData(session, {
session: ["activePlan", "messageCount"],
user: ["preferences"],
});
// Typed mode with schema validation:
const clientData = useClientData(session, {
session: {
activePlan: activePlanSchema,
},
});
useAction — Low-Level
For direct action execution without session management:
const { execute, loading, error } = useAction({
flowKind: "my-app",
action: "chat",
userId: "devuser",
});
useRequestStream — Direct Stream Access
const { items, status, isStreaming } = useRequestStream({
requestId,
filter: { itemTypes: ["message", "component"] },
});
Rendering Items
ItemRenderer and ItemsRenderer
import { ItemRenderer, ItemsRenderer } from "@flow-state-dev/react";
// Render all items. Sub-agent items are filtered out of the default
// conversation view — pass `showSubAgents` to surface them inline.
<ItemsRenderer items={session.items} />
// Show sub-agent items in the main stream (e.g., for a debug view).
<ItemsRenderer items={session.items} showSubAgents />
// Render a single item
<ItemRenderer item={item} />
Custom Renderers
All custom renderers receive { item } as their prop:
import type { MessageItem } from "@flow-state-dev/core/items";
function ChatMessage({ item }: { item: MessageItem }) {
return (
<div className={item.role === "user" ? "user-msg" : "assistant-msg"}>
{item.content.map((part, i) => (
<span key={i}>{part.text}</span>
))}
</div>
);
}
Suppressing Built-in Renderers
Pass false to suppress a type:
const renderers = {
status: false, // Don't render status items
reasoning: false, // Don't render reasoning items
};
Typical Pattern
function ChatUI() {
const flow = useFlow({ autoCreateSession: true });
const session = useSession(flow.activeSessionId);
return (
<div>
{/* Render items */}
{session.items.map((item) => (
<ItemRenderer key={item.id} item={item} />
))}
{/* Input form */}
<form onSubmit={(e) => {
e.preventDefault();
const msg = new FormData(e.currentTarget).get("message") as string;
session.sendAction("chat", { message: msg });
e.currentTarget.reset();
}}>
<input name="message" />
<button disabled={session.isStreaming}>Send</button>
</form>
</div>
);
}