Building AI Chat Interfaces in React: Streaming, Retries, and UX

Suman SharmaSuman Sharma

I lead frontend at dmclounge.ai, where chat-style AI interfaces are core to the product. The model gets the attention, but the interface decides whether people trust it.

Model the state properly

type Message = | { id: string; role: "user"; content: string } | { id: string; role: "assistant"; status: "streaming"; content: string } | { id: string; role: "assistant"; status: "done"; content: string } | { id: string; role: "assistant"; status: "error"; content: string; error: string };

A discriminated union stops you rendering a "retry" button on a message that's still streaming.

Streaming

On the client, read the response body stream and append chunks:

async function streamReply(prompt: string, signal: AbortSignal, onChunk: (t: string) => void) { const res = await fetch("/api/chat", { method: "POST", body: JSON.stringify({ prompt }), signal, }); if (!res.ok || !res.body) throw new Error(`HTTP ${res.status}`); const reader = res.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; onChunk(decoder.decode(value, { stream: true })); } }

Libraries like the Vercel AI SDK handle this plumbing well if you'd rather not.

Stop and retry

  • Stop: an AbortController per request. Also stop the upstream model call on the server, or you keep paying for tokens nobody sees.
  • Retry: re-send the same user message, and replace the failed assistant message rather than appending a new one.
  • Edit and resend: let users fix their prompt.

Rendering output safely

  • Use a markdown renderer without raw HTML, or sanitise.
  • Syntax-highlight code blocks and add a copy button.
  • Don't auto-open links from model output without user action.

UX details that build trust

  • Disclose it's AI. This is also an EU AI Act transparency duty.
  • Auto-scroll only if the user is at the bottom. Don't yank them away while reading.
  • Show sources when answers come from retrieval (RAG).
  • Clear error messages with a retry button, not a silent failure.
  • Accessible: announce new messages with an aria-live region, keep everything keyboard-operable.

Frequently asked questions

How do I stream LLM responses in React?

Have your server stream the model's output (for example as a ReadableStream or server-sent events), read the stream on the client, and append chunks to the current assistant message as they arrive.

How do I let users stop a response?

Use an AbortController for the fetch request, and make sure your server stops the upstream model request when the client disconnects.

Is it safe to render model output as markdown?

Only with a markdown renderer that doesn't allow raw HTML, or with sanitisation. Model output can contain anything, including content injected via prompts.

Contact

Get in Touch

Want to call me? book a call and I'll respond in the available time slot. I will ignore all soliciting.