# Stream LLM text in a Next.js App Router app

> Stream model output from a Next.js Route Handler with a Web ReadableStream, read it in a client component, and reveal it word by word without layout jumps.

Published 2026-11-04. Facts checked 2026-11-04. Source: https://pixilui.com/guides/streaming-text-nextjs

In the Next.js App Router, stream the model's text from a Route Handler by returning a `Response` built on a Web `ReadableStream`. Read it in a client component with `fetch` and a stream reader, append each chunk to one string in state, and pass that string to a streaming text component with `streaming` set until the reader finishes. Suspense and `loading.js` stream server-rendered UI; they are not for token streams.

## How Next.js streams

The Next.js [Streaming](https://nextjs.org/docs/app/guides/streaming) guide covers two kinds. React and Suspense stream server-rendered UI: a page sends its shell, then fills in sections as their data resolves. Outside React rendering, [Route Handlers](https://nextjs.org/docs/app/getting-started/route-handlers) stream raw responses with the Web Streams API, which is what a model's tokens need. The guide's own example returns `new Response(stream)` with a `text/plain` content type.

The component that shows the text holds state and runs effects, so it is a client component, marked with [`"use client"`](https://nextjs.org/docs/app/api-reference/directives/use-client). The page around it can stay a Server Component.

The same guide lists what breaks streaming after your code is right: reverse proxies such as Nginx that buffer by default (set `X-Accel-Buffering: no`), compression that waits for enough bytes, serverless platforms where streaming is not the default, and Safari, which buffers until 1,024 bytes have arrived.

## Which Next.js piece does what

| Job | Next.js mechanism | Runs on |
| --- | --- | --- |
| Call the model, stream its text | Route Handler returning a `ReadableStream` | Server |
| Read the stream, hold the text | `fetch`, `response.body.getReader()`, `useState` | Client component |
| Reveal words without reflow | `StreamingText` with `text` and `streaming` | Client component |
| Show the wait before the first token | `ThinkingState` with `startedAt` | Client component |
| Stream the rest of the page | Suspense or `loading.js` | Server Component |

## The code

The Route Handler streams plain text. This one uses the AI SDK's `textStream`; any SDK that yields text chunks works the same way.

```ts
// app/api/answer/route.ts
import { streamText } from "ai";

export async function POST(req: Request) {
  const { question }: { question: string } = await req.json();
  const result = streamText({ model: "openai/gpt-5.5", prompt: question });

  return new Response(result.textStream.pipeThrough(new TextEncoderStream()), {
    headers: {
      "Content-Type": "text/plain; charset=utf-8",
      "X-Content-Type-Options": "nosniff",
      "X-Accel-Buffering": "no",
    },
  });
}
```

The client component reads the body, keeps the whole text so far in state and hands it to the component on every chunk.

```tsx
// app/answer/answer.tsx
"use client";

import { useRef, useState } from "react";
import { StreamingText } from "@/components/pixilui/streaming-text";
import { ThinkingState } from "@/components/pixilui/thinking-state";

export function Answer({ question }: { question: string }) {
  const [text, setText] = useState("");
  const [streaming, setStreaming] = useState(false);
  const [startedAt, setStartedAt] = useState<number>();
  const abort = useRef<AbortController | null>(null);

  async function ask() {
    abort.current?.abort();
    abort.current = new AbortController();
    setText("");
    setStreaming(true);
    setStartedAt(Date.now());
    try {
      const response = await fetch("/api/answer", { method: "POST", body: JSON.stringify({ question }), signal: abort.current.signal });
      const reader = response.body!.pipeThrough(new TextDecoderStream()).getReader();
      for (;;) {
        const { value, done } = await reader.read();
        if (done) break;
        setText((so) => so + value); // the whole text so far, not the chunk
      }
    } catch (error) {
      if ((error as Error).name !== "AbortError") setText((so) => so || "The answer did not arrive. Ask again.");
    } finally {
      setStreaming(false);
    }
  }

  return (
    <section aria-live="polite">
      <button type="button" onClick={streaming ? () => abort.current?.abort() : ask}>
        {streaming ? "Stop" : "Ask"}
      </button>
      {streaming && !text ? <ThinkingState label="Reading the question" startedAt={startedAt} /> : null}
      {text ? <StreamingText text={text} streaming={streaming} pauseOnHover /> : null}
    </section>
  );
}
```

Use it from a Server Component page: `<Answer question="Why did checkout errors rise on Tuesday?" />`. Install both with `npx shadcn@latest add @pixilui/streaming-text @pixilui/thinking-state`.

## What the component handles that the reader loop does not

Chunks arrive in uneven bursts and can split a word in two. Rendering `text` directly reflows the paragraph on every chunk. `StreamingText`:

- **Reveals whole words** at about 50 ms a word, and holds back a half-arrived one.
- **Never moves a word once it is on screen**, so earlier lines do not shift.
- **Rewinds to the shared prefix** if the text changes in the middle, for a regenerate.
- **Drops the caret** once `streaming` is false and the last word is shown.
- **Respects reduced motion**: words appear as they arrive, with no rise.

## When the framework already ships a UI

Next.js ships no text-streaming component; it gives you the transport. If you use the AI SDK's `useChat`, it reads the stream for you and gives you `message.parts`; pass the text part to the same component, as in [streaming text with the Vercel AI SDK](/guides/streaming-text-vercel-ai-sdk). For answers that need full markdown (tables, lists, code blocks), use a markdown renderer that streams; `StreamingText` renders plain text with inline code.

## When not to use this

- **Short answers.** A two-word label can render at once.
- **Server-rendered content that is slow, not streamed.** Use Suspense and `loading.js`; a token stream is the wrong tool for a slow database query.
- **Structured output.** Stream JSON into a form or table, not into a paragraph.

## Questions

### How do I stream an LLM response in the Next.js App Router?

Return a `Response` built on a `ReadableStream` from a Route Handler, as the Next.js Streaming guide shows, and read it on the client with `response.body.getReader()`. Keep the text in state in a client component and append each chunk.

### Why does my Next.js stream arrive all at once?

Something between server and browser is buffering. The Next.js Streaming guide names reverse proxies like Nginx (set `X-Accel-Buffering: no`), compression, serverless platforms where streaming must be enabled, and Safari, which waits for 1,024 bytes.

### Can a Server Component stream tokens?

Server Components stream rendered UI through Suspense, not a growing string. The text needs state and effects, so the part that shows tokens is a client component, which a Server Component page can render.

### Should I use loading.js for an AI answer?

No. `loading.js` shows a fallback while a route segment renders on the server. For the wait before the first token, show a thinking line in the client component that started the request.

## Related

- [How to stream LLM text in React without the layout jumping](/guides/stream-llm-text-react-without-layout-jump): why streamed text jumps, framework aside.
- [Streaming text with the Vercel AI SDK](/guides/streaming-text-vercel-ai-sdk): the same component on `useChat`.
- [How to show an AI model's reasoning or thinking state](/guides/show-ai-reasoning-thinking-state): the line before the first token.

## Sources

- [Next.js: Streaming](https://nextjs.org/docs/app/guides/streaming), read 4 November 2026
- [Next.js: Route Handlers](https://nextjs.org/docs/app/getting-started/route-handlers), read 4 November 2026
- [Next.js: use client](https://nextjs.org/docs/app/api-reference/directives/use-client), read 4 November 2026
- [AI SDK: streamText](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text), read 4 November 2026
