Skip to content

Multiple Streamable UIs Disappear in Production Builds When Calling .done() #1558

@amcclure-super

Description

@amcclure-super

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>
  };
}

Metadata

Metadata

Assignees

Labels

ai/rscbugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions