The Vercel AI SDK’s useChat hook gets a streaming chat UI running in minutes, but the default example leaves out two controls users immediately expect: a way to halt a runaway generation and a way to retry a bad one. This usechat stop regenerate button tutorial shows how to add stop and regenerate buttons to your useChat UI without forking the library or writing custom abort logic. We’ll build on the standard React hook and a Next.js route handler, and point out the exact state fields you need to watch.
Step 1: Inspect the useChat API surface
Before writing UI, know what the hook already gives you. In AI SDK v3 (ai/react) and v4 (@ai-sdk/react), useChat returns an object with the fields you need for interruption and retry:
import { useChat } from 'ai/react';
const {
messages,
input,
handleInputChange,
handleSubmit,
isLoading,
stop,
reload,
error,
} = useChat({ api: '/api/chat' });
isLoadingistruewhile a request is in flight or streaming.stop()aborts the current fetch and terminates the stream. The partial assistant message stays inmessages.reload()resends the conversation up to the last user message and replaces the trailing assistant message with a fresh completion.errorholds the last thrown error from a failed request.
If you’re on an older version, stop and reload may be missing—upgrade to ai@^3.0.0 before continuing. This usechat stop regenerate button tutorial assumes those methods exist.
Step 2: Render a Stop button during streaming
The simplest correct implementation renders a Stop button only while isLoading is true. Call stop() on click. No manual AbortController needed; the hook owns the signal.
function Chat() {
const { messages, input, handleInputChange, handleSubmit, isLoading, stop } =
useChat({ api: '/api/chat' });
return (
<div>
{messages.map((m) => (
<div key={m.id}>
<strong>{m.role}:</strong> {m.content}
</div>
))}
<form onSubmit={handleSubmit}>
<input value={input} onChange={handleInputChange} />
<button type="submit" disabled={isLoading}>
Send
</button>
{isLoading && (
<button type="button" onClick={stop}>
Stop
</button>
)}
</form>
</div>
);
}
Clicking Stop triggers fetch abort. The stream ends, isLoading flips to false, and the partial text remains visible. Users can then edit their last message or regenerate.
Step 3: Add a Regenerate button to the last assistant message
reload() is the built-in retry. Expose it as a button attached to the last assistant message. Disable it while streaming to avoid overlapping requests.
function Chat() {
const { messages, input, handleInputChange, handleSubmit, isLoading, stop, reload } =
useChat({ api: '/api/chat' });
const lastMessage = messages[messages.length - 1];
return (
<div>
{messages.map((m, i) => (
<div key={m.id}>
<strong>{m.role}:</strong> {m.content}
{m.role === 'assistant' &&
i === messages.length - 1 &&
!isLoading && (
<button type="button" onClick={() => reload()}>
Regenerate
</button>
)}
</div>
))}
<form onSubmit={handleSubmit}>
<input value={input} onChange={handleInputChange} />
<button type="submit" disabled={isLoading}>
Send
</button>
{isLoading && (
<button type="button" onClick={stop}>
Stop
</button>
)}
</form>
</div>
);
}
One caveat: reload() replays the entire message history to the server. If your backend truncates or summarizes context, the regenerated answer may differ from what you expect. Keep your route stateless and pass the full messages array, as the SDK does by default.
Step 4: Handle errors and partial generations
A stopped stream is not an error, but a network failure is. Surface error and let the user retry.
{messages.map((m, i) => (
<div key={m.id}>
<strong>{m.role}:</strong> {m.content}
{m.role === 'assistant' && i === messages.length - 1 && (
<>
{!isLoading && (
<button type="button" onClick={() => reload()}>
Regenerate
</button>
)}
{error && (
<span style={{ color: 'red' }}>
{' '}Failed to generate. <button onClick={() => reload()}>Retry</button>
</span>
)}
</>
)}
</div>
))}
If the user pressed Stop, error is null and the partial message shows. Regenerate will replace that partial with a new stream. If the request failed mid-stream, error is set and the same button recovers. Don’t call reload() while isLoading is true—it’s a no-op or throws in some versions.
Step 5: Wire the backend route
The frontend is useless without a streaming endpoint. Here is a minimal Next.js App Router handler using the AI SDK server utilities. In this usechat stop regenerate button tutorial we keep the model call OpenAI-compatible so you can swap providers freely.
// app/api/chat/route.ts
import { openai } from '@ai-sdk/openai';
import { streamText } from 'ai';
export const runtime = 'edge';
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: openai('gpt-4o-mini'),
messages,
});
return result.toDataStreamResponse();
}
If you point this route at n4n.ai, the same OpenAI-compatible endpoint works unchanged and gives you automatic fallback when a provider is rate-limited or degraded—no client changes required for the stop/regenerate flow.
For local dev, set OPENAI_API_KEY. The streamText call respects the AbortSignal from the incoming request, so when the client calls stop(), the server stream cancels cleanly.
Step 6: Verify end-to-end
Run the app and exercise the controls:
npm run devand open the chat page.- Type a prompt that yields a long response (e.g., “Write a 500-word essay on TCP congestion control”).
- Confirm tokens stream in and the Stop button appears.
- Click Stop. The stream halts, the partial text remains, and Stop disappears.
- Click Regenerate on the last assistant message. A new stream starts, replacing the partial text.
- Kill the network or throw inside the route to trigger
error. Confirm the Retry span appears and reload recovers.
Open the browser network tab: while streaming, the request shows type fetch with a pending state. After Stop, the request is canceled (red status). That confirms the abort propagated.
Practical notes
- Don’t render a global Regenerate button for every assistant turn.
reload()only makes sense for the trailing message; older ones would be ambiguous. - If you customize
onErrororonFinishinuseChat, preserve the defaultreload/stopbindings. Overridingbodyorheadersis fine, but don’t swallow the abort. - For multi-model UIs, track which model produced each message;
reload()will reuse the same model unless you passoptionstouseChatdynamically. stop()does not clearinput. Users can resend the same prompt or edit it before regenerating.
Following this usechat stop regenerate button tutorial gives you a chat surface that behaves like the commercial apps: interruptible, recoverable, and honest about failure states. The whole addition is under 30 lines of React and requires zero changes to the streaming protocol.