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
- Layout: scrollable message list, composer (textarea + send), optional stop button.
- State: messages array and pending flag (Chatbot State and Conversation UI in React).
- On submit: append the user message, set pending,
POSTto/api/chat. - On success: append assistant message; on failure: show an error banner and keep the draft.
- Escape or sanitise any Markdown HTML before
dangerouslySetInnerHTML. - Add streaming later (Streaming AI Responses for Chat UIs) once the non-stream path works.
- 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.