Overview
Primitives for building streaming UI with React.
The @stream.ui/react package provides primitives for handling partial/streaming data in React applications.
Installation
npm install @stream.ui/reactpnpm add @stream.ui/reactyarn add @stream.ui/reactbun add @stream.ui/reactUsage
import { Stream } from "@stream.ui/react";
function MyComponent() {
const { object, isLoading, error } = useObject({ schema: mySchema });
return (
<Stream.Root data={object} isLoading={isLoading} error={error}>
<Stream.Field fallback={<Skeleton />}>
<h1>{object?.title}</h1>
</Stream.Field>
<Stream.List fallback={<ItemsSkeleton />}>
{object?.items?.map(item => <Item key={item.id} {...item} />)}
</Stream.List>
<Stream.When loading>
<Spinner />
</Stream.When>
<Stream.When error>
{(err) => <ErrorMessage error={err} />}
</Stream.When>
</Stream.Root>
);
}Primitives
| Primitive | Description |
|---|---|
Stream.Root | Provides context with streaming data and state |
Stream.Field | Renders content with fallback when undefined |
Stream.List | Renders arrays that grow as data streams in |
Stream.When | Conditionally renders based on stream state |
How It Works
When you use useObject from AI SDK, data arrives incrementally:
t=0: undefined
t=1: { title: "Hello" }
t=2: { title: "Hello", items: [] }
t=3: { title: "Hello", items: [{ id: 1 }] }
t=4: { title: "Hello", items: [{ id: 1 }, { id: 2 }] }
...The primitives handle this automatically:
- Stream.Root tracks the state (loading → streaming → complete)
- Stream.Field shows fallback until values are defined
- Stream.List renders items as they arrive
- Stream.When shows different UI based on state
States
| State | Condition |
|---|---|
idle | No data, not loading |
loading | Loading, no data yet |
streaming | Loading, partial data exists |
complete | Not loading, data exists |
error | An error occurred |
TypeScript
The primitives work with your typed objects from useObject. Since you access your data directly (e.g., object?.title), you get full type safety and autocompletion from your schema.
import { Stream } from "@stream.ui/react";
import { experimental_useObject as useObject } from "@ai-sdk/react";
// Your schema defines the type
const articleSchema = z.object({
title: z.string(),
summary: z.string(),
sections: z.array(z.object({ heading: z.string(), content: z.string() })),
});
function ArticleCard() {
const { object, isLoading, error } = useObject({
api: "/api/article",
schema: articleSchema,
});
return (
<Stream.Root data={object} isLoading={isLoading} error={error}>
{/* object?.title is typed as string | undefined */}
<Stream.Field fallback={<Skeleton />}>
<h1>{object?.title}</h1>
</Stream.Field>
{/* object?.summary is typed as string | undefined */}
<Stream.Field fallback={<Skeleton />}>
<p>{object?.summary}</p>
</Stream.Field>
{/* object?.sections is typed as the array from schema */}
<Stream.List fallback={<Skeleton />}>
{object?.sections?.map(section => <Section key={section.heading} {...section} />)}
</Stream.List>
</Stream.Root>
);
}Types are also exported for advanced use cases:
import type {
DeepPartial,
StreamState,
StreamContextValue,
StreamRootProps,
StreamFieldProps,
StreamListProps,
StreamWhenProps,
} from "@stream.ui/react";