-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Open
Labels
Description
Description
When nesting createStreamableUI values inside another createStreamableUI, the final result disappears in production builds. The issue does not surface in local development.
Here is a reproducible example which works in dev but not after running pnpm build
: https://github.com/amcclure-super/ai-streamableUI
Here are a couple videos. The first video in a dev build shows that the values do not disappear after marking ui.done(). The second video shows the values disappearing after ui.done() is called in a production build.
In dev, the UI does not disappear after ui.done():
2024-05-10.16-27-27.mp4
In a production build, the UI disappears after ui.done() is called:
2024-05-10.16-28-35.mp4
Code example
Full repo: https://github.com/amcclure-super/ai-streamableUI
Relevant code:
// page.tsx
"use client";
import { useState } from "react";
import { ClientMessage } from "./actions";
import { useActions, useUIState } from "ai/rsc";
import { nanoid } from "nanoid";
export default function Home() {
const [input, setInput] = useState<string>("");
const [conversation, setConversation] = useUIState();
const { submitUserMessage } = useActions();
return (
<div>
<div>
{conversation.map((message: ClientMessage) => (
<div key={message.id}>
{message.role}: {message.display}
</div>
))}
</div>
<div>
<input
type="text"
value={input}
onChange={(event) => {
setInput(event.target.value);
}}
/>
<button
onClick={async () => {
setConversation((currentConversation: ClientMessage[]) => [
...currentConversation,
{ id: nanoid(), role: "user", display: input }
]);
const message = await submitUserMessage(input);
setConversation((currentConversation: ClientMessage[]) => [
...currentConversation,
message
]);
}}
>
Send Message
</button>
</div>
</div>
);
}
// actions.tsx
import "server-only";
import { createAI, createStreamableUI } from "ai/rsc";
import { ReactNode } from "react";
export interface ServerMessage {
role: "user" | "assistant";
content: string;
}
export interface ClientMessage {
id: string;
role: "user" | "assistant";
display: ReactNode;
}
export async function submitUserMessage(userInput: string) {
"use server";
let ui, uiStreams;
try {
ui = createStreamableUI(<div>Loading...</div>);
(async () => {
try {
ui.update(<div>UI Update 1</div>);
uiStreams = Array.from({ length: 3 }).map((_, i) => {
return createStreamableUI(<div>Nested {i}</div>);
});
await new Promise((resolve) => setTimeout(() => resolve(true), 3000)); // wait 3 seconds
ui.update(
<div>
Composed UI{" "}
{uiStreams.map((u) => {
return u.value;
})}
</div>
);
await new Promise((resolve) => setTimeout(() => resolve(true), 3000)); // wait 3 seconds
} catch (e) {
} finally {
for (let i = 0; i < uiStreams?.length ?? 0; i++) {
uiStreams[i].done();
}
ui.done();
}
})();
} catch (e) {}
return {
id: Date.now(),
display: <div>{ui.value}</div>
};
}