Minimal example
The smallest possible integration. useAIChat with plain HTML — no @react-ai-stream/ui dependency.
Source: apps/nextjs-basic (opens in a new tab)
Run locally
cd apps/nextjs-basic
cp .env.local.example .env.local # add GROQ_API_KEY
pnpm dev # http://localhost:3001The full page
app/page.tsx
'use client'
import { useRef, useState } from 'react'
import { useAIChat } from '@react-ai-stream/react'
export default function Page() {
const { messages, sendMessage, loading, stop, error } = useAIChat({
endpoint: '/api/chat',
})
const [input, setInput] = useState('')
const bottomRef = useRef<HTMLDivElement>(null)
function handleSubmit(e: React.FormEvent) {
e.preventDefault()
const text = input.trim()
if (!text || loading) return
setInput('')
sendMessage(text).then(() => {
bottomRef.current?.scrollIntoView({ behavior: 'smooth' })
})
}
return (
<div style={{ maxWidth: 680, margin: '40px auto', padding: '0 16px' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, marginBottom: 16 }}>
{messages.map((msg) => (
<div
key={msg.id}
style={{
alignSelf: msg.role === 'user' ? 'flex-end' : 'flex-start',
background: msg.role === 'user' ? '#2563eb' : '#f3f4f6',
color: msg.role === 'user' ? '#fff' : '#111',
borderRadius: 12,
padding: '10px 14px',
maxWidth: '80%',
whiteSpace: 'pre-wrap',
}}
>
{msg.content}
</div>
))}
{loading && <div style={{ color: '#9ca3af' }}>Thinking…</div>}
{error && <div style={{ color: '#ef4444' }}>Error: {error}</div>}
<div ref={bottomRef} />
</div>
<form onSubmit={handleSubmit} style={{ display: 'flex', gap: 8 }}>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
disabled={loading}
placeholder="Type a message…"
style={{ flex: 1, padding: '10px 14px', borderRadius: 8, border: '1px solid #d1d5db' }}
/>
{loading
? <button type="button" onClick={stop}>Stop</button>
: <button type="submit" disabled={!input.trim()}>Send</button>}
</form>
</div>
)
}What this demonstrates
useAIChatneeds zero UI package — install only@react-ai-stream/reactmessages,loading,error,stopare all you need to build any chat UI- The hook is stateless from the component's perspective — no
useStatefor messages
Next step
See Custom UI for a Tailwind implementation with a model switcher sidebar.