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.items; // All 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
// Actions:
await session.sendAction("chat", { message: "Hello!" });
session.refresh(); // Force refetch
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
<ItemsRenderer items={session.items} />
// 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>
);
}