Streaming AI Responses

Render AI output token by token as it streams in, so users see progress instead of a spinner.

TL;DR

  1. Read the response body with a ReadableStream reader.
  2. Decode chunks and append tokens to state as they arrive.
  3. Show partial text immediately instead of a spinner.

Read the Stream

    Call the Endpoint

    POST the prompt to your own streaming route.

    const res = await fetch('/api/chat', {
      method: 'POST',
      body: JSON.stringify({ prompt }),
    });
    Get a Reader

    Read the response body as a byte stream.

    const reader = res.body.getReader();
    const decoder = new TextDecoder();
    Loop the Chunks

    Read until done, decoding each chunk to text.

    while (true) {
      const { value, done } = await reader.read();
      if (done) break;
      handle(decoder.decode(value));
    }

Parse SSE Events

    Split on Lines

    Each event is a line that starts with data:.

    for (const line of chunk.split('\n')) {
      if (!line.startsWith('data: ')) continue;
      const json = line.slice(6);
    }
    Parse the Token

    Pull the text delta out of each JSON event.

    const evt = JSON.parse(json);
    const token = evt.delta ?? '';
    Stop at [DONE]

    A sentinel line marks the end of the stream.

    if (json.trim() === '[DONE]') return;

Append to State

    Append Tokens

    Add each token to the growing message text.

    setText((prev) => prev + token);
    Buffer in a Ref

    Collect tokens to avoid a render per token.

    bufferRef.current += token;
    Flush on a Frame

    Push the buffer to state once per paint.

    requestAnimationFrame(() =>
      setText(bufferRef.current));

Clean Up

    Pass a Signal

    Give fetch a signal so a new send can cancel.

    fetch(url, { signal: controller.signal });
    Release the Reader

    Cancel the reader if the user leaves.

    reader.cancel();
    Catch Aborts

    Ignore AbortError; surface real failures.

    catch (e) {
      if (e.name !== 'AbortError') setError(e);
    }

Tips

  1. Pass fetch an abort signal and read the reader in a loop, updating state on each chunk so React re-renders the growing text.
  2. If tokens arrive faster than the browser paints, buffer them in a ref and flush to state once per animation frame.

Warnings

  1. Never call the AI provider directly from the browser; stream through your own routeHandler so keys stay on the server.
  2. Always handle a dropped connection and release the reader, or a half-finished stream can leave the UI stuck mid-answer.

In Practice

FAQ