Tutorials·8 min read

How to Build an AI Chatbot in React with Streaming Responses

Step-by-step React chat UI that consumes a streaming AI API through your backend, with message state, abort handling, and accessibility basics.

By Published

React powers countless dashboards and customer portals. Adding an AI chatbot usually means three pieces: a secure backend proxy, a streaming HTTP response, and client state that appends tokens as they arrive. This guide focuses on the React layer assuming you already expose POST /api/chat that streams SSE from your AI API (see Next.js integration or Python FastAPI tutorial).

Component structure

Keep chat state in a single array of { role, content } messages. Derive loading state from whether a stream is open. Scroll the viewport when messages change.

"use client";

import { useCallback, useRef, useState } from "react";

type Message = { role: "user" | "assistant"; content: string };

export function ChatPanel() {
  const [messages, setMessages] = useState<Message[]>([]);
  const [input, setInput] = useState("");
  const [streaming, setStreaming] = useState(false);
  const abortRef = useRef<AbortController | null>(null);

  const send = useCallback(async () => {
    const text = input.trim();
    if (!text || streaming) return;

    const next: Message[] = [...messages, { role: "user", content: text }];
    setMessages(next);
    setInput("");
    setStreaming(true);
    setMessages((m) => [...m, { role: "assistant", content: "" }]);

    abortRef.current = new AbortController();
    const res = await fetch("/api/chat", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ messages: next }),
      signal: abortRef.current.signal,
    });

    if (!res.ok || !res.body) {
      setStreaming(false);
      return;
    }

    const reader = res.body.getReader();
    const decoder = new TextDecoder();
    let assistant = "";

    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      assistant += decoder.decode(value, { stream: true });
      setMessages((m) => {
        const copy = [...m];
        copy[copy.length - 1] = { role: "assistant", content: assistant };
        return copy;
      });
    }

    setStreaming(false);
  }, [input, messages, streaming]);

  return (
    <div>
      {/* render messages; disable send while streaming */}
    </div>
  );
}

Adapt parsing if your API wraps SSE data: lines instead of raw text — the state update pattern stays the same.

Never put API keys in React

All calls go to your origin. Keys live only on the server. This is non-negotiable; see Managing API keys securely.

Stop generation

Wire a "Stop" button to abortRef.current?.abort(). Reset streaming in a finally block so the UI never sticks in loading state.

Markdown and safety

Assistant output often includes code fences. Use a markdown renderer with sanitization (e.g. rehype-sanitize) before dangerouslySetInnerHTML. For customer-facing bots, add a short system policy on the server — not in the client bundle.

SEO and marketing sites

Marketing pages rarely need live chat on the homepage for SEO; they need help content that ranks. Pair product UI work with guides like AI API for SaaS startups so organic traffic lands on indexed articles that link into your app.

Testing

  • Mock fetch with a ReadableStream that emits chunks slowly
  • Snapshot the final message array after stream end
  • Test abort mid-stream leaves a partial assistant message (acceptable UX)

Summary

React chatbots are mostly stream plumbing plus disciplined state updates. Proxy on the server, stream over HTTP, abort on demand, and sanitize rendered markdown. That stack ships fast and scales to production SaaS with the patterns in our production AI features guide.

Start building

Start building for $25/month

Flat-rate API access with fair usage included. GPT-5, Claude Sonnet 4, and Gemini 2.5 Pro. Straightforward REST API with code examples and a built-in tester.

Flat-rate AI API pricing. $25/month.

Create your API key →