EXPLAINER

Building AI Interfaces with React

Build a React chat UI with a message list, an input box, loading and error states, and calls to your backend — never with a model API key in the client bundle.

Short answer: A React AI interface is a message list + composer + loading/error states talking to your own API. Keep secrets on the server; keep UI state explicit.

What is it?

An AI chat interface in React is an ordinary web UI: it renders conversation turns, accepts user input, and displays assistant replies. The model lives behind your backend. You do not need a specific meta-framework (Next.js, Remix, etc.) to learn the pattern — a single-page React app calling fetch is enough to start.

Why it matters

Users judge AI products by the interface: clear pending states, readable errors, and interruptible streams. A solid UI also makes it obvious when the API key must not be in the browser (JavaScript for AI Applications).

How to do it

  1. Layout: scrollable message list, composer (textarea + send), optional stop button.
  2. State: messages array and pending flag (Chatbot State and Conversation UI in React).
  3. On submit: append the user message, set pending, POST to /api/chat.
  4. On success: append assistant message; on failure: show an error banner and keep the draft.
  5. Escape or sanitise any Markdown HTML before dangerouslySetInnerHTML.
  6. Add streaming later (Streaming AI Responses for Chat UIs) once the non-stream path works.
  7. Disable the send button while pending; allow Escape/Stop to abort.

Example

function ChatApp() {
  const [messages, setMessages] = React.useState([]);
  const [pending, setPending] = React.useState(false);
  const [error, setError] = React.useState(null);
  const [draft, setDraft] = React.useState("");

  async function send() {
    const text = draft.trim();
    if (!text || pending) return;
    setDraft(""); setError(null); setPending(true);
    setMessages(m => [...m, { role: "user", content: text }]);
    try {
      const res = await fetch("/api/chat", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ message: text }),
      });
      if (!res.ok) throw new Error("Request failed");
      const data = await res.json();
      setMessages(m => [...m, { role: "assistant", content: data.text }]);
    } catch (e) {
      setError(String(e.message || e));
    } finally {
      setPending(false);
    }
  }

  return (
    <div>
      <ul>{messages.map((m, i) => <li key={i}><strong>{m.role}:</strong> {m.content}</li>)}</ul>
      {pending && <p>Thinking…</p>}
      {error && <p role="alert">{error}</p>}
      <textarea value={draft} onChange={e => setDraft(e.target.value)} />
      <button disabled={pending} onClick={send}>Send</button>
    </div>
  );
}

When to use it

Use React when your product already has a React design system or you need rich client state. Plain HTML is fine for internal tools. Pair with Node or another backend for secrets.

Practical notes

Accessibility and polish separate a demo from a product. The composer should be a labelled field; errors should use 'role="alert"'. Support Enter to send and Shift+Enter for newline if that matches your users. Provide a clear empty state that suggests example questions from your domain. Persist draft text if the user refreshes mid-composition. When rendering assistant Markdown, use a sanitiser and open links in a new tab with 'rel="noopener noreferrer"'. Measure time-to-first-token and total response time; the UI is the right place to display a subtle timer for power users. Keep styling consistent with the rest of GoVibeTool-like products: readable contrast, large tap targets on mobile, and no reliance on hover-only controls.

Common mistakes

  • Putting the model API key in Vite env vars prefixed for the client.
  • No loading state — users double-submit.
  • Replacing the whole message list instead of appending.
  • Rendering untrusted Markdown as raw HTML.