useCompletion is for autocomplete-style single responses, not multi-turn chat. The client and server must agree on the stream protocol or parsing fails.
Client (app/page.tsx, 'use client'):
import { useCompletion } from '@ai-sdk/react';
export default function Page() {
const { completion, input, handleInputChange, handleSubmit, isLoading, error } = useCompletion({
api: '/api/completion',
});
return (
[form onSubmit={handleSubmit}]
[input value={input} onChange={handleInputChange} /]
[button type="submit"]Submit[/button]
[div]{completion}[/div]
[/form]
);
}
Server (app/api/completion/route.ts):
import { streamText } from 'ai';
export const maxDuration = 30;
export async function POST(req: Request) {
const { prompt } = await req.json();
const result = streamText({ model: 'your-model-id', prompt });
return result.toUIMessageStreamResponse();
}
Checklist:
1. useCompletion still manages input/handleSubmit for you (unlike useChat v5+). Do not mix the two hooks' patterns.
2. The server returns a UI message stream via result.toUIMessageStreamResponse(). The older toTextStream/createTextStreamResponse pair is for raw text protocols only.
3. If you serve a raw text stream instead, the client must opt into it with new TextStreamChatTransport on useCompletion, or you get Failed to parse stream string errors.
4. completion holds the streamed text; isLoading and error cover the request lifecycle. Render error instead of swallowing it.
5. A classic bug: calling setState during render from completion updates, which throws Maximum update depth exceeded. Derive UI in effects or memo, never setState inline in the render body.