n4nAI

useChat vs useCompletion in the Vercel AI SDK

A practical comparison of useChat and useCompletion in the Vercel AI SDK, covering when to use each hook for streaming LLM responses in production applications.

n4n Team5 min read1,050 words

Audio narration

Coming soon — every post will get a voice note here.

The Vercel AI SDK gives you two primary hooks for streaming LLM output: useChat and useCompletion. Choosing between them isn’t about which is “better” — it’s about matching the abstraction level to your UI pattern. useChat manages conversation state for you; useCompletion hands you a raw stream and gets out of the way. This comparison breaks down the concrete differences so you can decide without guessing.

Core abstraction difference

useChat is a conversation manager. It maintains a messages array with roles (user, assistant, system, tool), handles optimistic updates, assigns stable IDs, and exposes helpers like append, reload, and stop. You feed it a prompt and it returns the full message history plus a status enum (submitted, streaming, ready, error).

useCompletion is a stream consumer. It takes a prompt (string or message array), calls your API route, and returns completion (the accumulated text), complete (boolean), and error. No message history, no role management, no append helper. You own the input and output representation entirely.

// useChat — conversation-aware
const { messages, append, status } = useChat({
  api: '/api/chat',
  initialMessages: [{ role: 'user', content: 'Hello' }],
});

// useCompletion — raw stream
const { completion, complete, error } = useCompletion({
  api: '/api/complete',
  prompt: 'Summarize this article...',
});

If you’re building a chat interface, useChat saves you 200+ lines of boilerplate. If you’re building a summarizer, code generator, or any single-turn flow, useCompletion doesn’t force a message schema you don’t need.

Streaming behavior and backpressure

Both hooks consume ReadableStream responses from your API route (typically via streamText on the server). The difference is what they do with the chunks.

useChat parses the stream as a sequence of message parts: text-delta, tool-call, tool-result, finish. It reconstructs assistant messages incrementally, so messages[messages.length - 1].content grows in real time. It also handles tool-call streaming — the assistant message gets a toolInvocations array that populates as arguments arrive.

useCompletion concatenates text deltas into a single completion string. It ignores tool calls entirely; if your server streams tool invocations, they land in the raw stream but never surface through the hook. You’d need to parse the stream yourself using the lower-level useStream or a custom reader.

// Server side — both hooks consume this
const result = streamText({
  model: openai('gpt-4o'),
  messages,
  tools: { getWeather: weatherTool },
  onFinish: async ({ response }) => {
    // save to DB, etc.
  },
});
return result.toDataStreamResponse();

For chat with tools, useChat is the only hook that renders tool calls progressively without custom code. For pure text streaming, both work; useCompletion is lighter.

State management and persistence

useChat gives you a messages array you can persist directly to localStorage, IndexedDB, or a database. Each message has id, role, content, createdAt, and optional annotations (for tool calls, citations, etc.). The setMessages function lets you hydrate from storage or implement “fork conversation” patterns.

useCompletion gives you a string. Persistence is entirely on you — save the prompt, save the completion, reconstruct whatever UI state you need. This is simpler for ephemeral flows (form autocomplete, one-shot generation) but requires more code if you later decide to add history.

// Hydrating useChat from localStorage
const saved = JSON.parse(localStorage.getItem('chat-history') ?? '[]');
const { messages, setMessages } = useChat({
  initialMessages: saved,
  onFinish: (msg) => {
    localStorage.setItem('chat-history', JSON.stringify([...messages, msg]));
  },
});

// useCompletion — you build the history object
const [history, setHistory] = useState<Array<{prompt: string; result: string}>>([]);
const { completion, complete } = useCompletion({
  prompt: currentPrompt,
  onFinish: (text) => {
    setHistory(h => [...h, { prompt: currentPrompt, result: text }]);
  },
});

Error handling and recovery

useChat exposes error on the hook and an error property on individual messages. The reload function re-sends the last user message; stop aborts the in-flight request. Status transitions are predictable: submittedstreamingready (or error).

useCompletion returns a single error value and a stop function. No per-message granularity, no reload — you re-invoke by changing the prompt prop. If you need retry-with-backoff or per-message error UI, you build it.

// useChat — granular error UI
{messages.map(m => (
  <Message key={m.id} message={m} />
))}
{status === 'error' && <button onClick={reload}>Retry</button>}

// useCompletion — manual retry
const [prompt, setPrompt] = useState('');
const { completion, error, stop } = useCompletion({ prompt });
return (
  <>
    <textarea value={prompt} onChange={e => setPrompt(e.target.value)} />
    {error && <button onClick={() => setPrompt(prompt)}>Retry</button>}
    <div>{completion}</div>
  </>
);

Tool calling and structured output

This is where the hooks diverge most sharply. useChat natively supports tool calls streamed from the server. The assistant message gets a toolInvocations array with state: 'calling' | 'result', and the tool result appears as a separate tool role message when complete. The UI can render loading states for each tool call without extra code.

useCompletion does not expose tool calls. The stream from streamText includes them, but the hook only concatenates text deltas. If you need tool calls with useCompletion, drop down to useStream (the primitive both hooks build on) and parse the data stream protocol yourself.

// useChat renders this automatically
{
  id: 'msg-123',
  role: 'assistant',
  content: '',
  toolInvocations: [
    { toolCallId: 'call-1', toolName: 'getWeather', args: { city: 'SF' }, state: 'calling' },
    { toolCallId: 'call-1', toolName: 'getWeather', args: { city: 'SF' }, state: 'result', result: { temp: 58 } }
  ]
}

For agentic workflows where the model calls multiple tools per turn, useChat is the only viable choice without writing a custom stream parser.

Performance and bundle size

Both hooks are tree-shakeable. useChat imports more internal machinery (message reducers, ID generation, tool-call state machines) — roughly 3.5 kB gzipped vs. ~1.2 kB for useCompletion. In a typical Next.js app with the AI SDK already installed, the marginal cost is negligible. The real performance difference is in your render tree: useChat encourages mapping over a message array (virtualize for long histories), while useCompletion renders a single string.

Ecosystem integration

useChat integrates with the SDK’s Message type, createDataStreamResponse, and the experimental_telemetry hook for logging. It works with the Chat component from @vercel/ai/react for zero-config rendering. useCompletion integrates with completion metadata and the same telemetry, but has no pre-built UI components.

Both hooks respect the headers and credentials options for auth, and both forward signal from AbortController for cancellation. Neither hook manages provider fallbacks or model routing — that lives in your API route. If you’re using a gateway like n4n.ai that handles automatic fallback across 240+ models, the hook choice doesn’t affect that logic; your route just streams the response.

Comparison table

Dimension useChat useCompletion
Primary use case Multi-turn conversations, chat UIs Single-turn generation, summarization, autocomplete
State managed Full message array with roles, IDs, timestamps Accumulated completion string only
Streaming parsing Text deltas, tool calls, tool results, finish reasons Text deltas only
Tool call support Native, progressive rendering None (requires useStream + custom parser)
History persistence Direct (messages array is serializable) Manual (you define the schema)
Recovery helpers reload, stop, per-message error stop only, retry via prompt change
Bundle size (gzipped) ~3.5 kB ~1.2 kB
Built-in UI components Chat, Message from @vercel/ai/react None
TypeScript API UseChatHelpers<Message> UseCompletionHelpers<string>

Which to choose

Choose useChat when:

  • Building any chat interface (support bot, coding assistant, roleplay)
  • The model calls tools and you need to show those calls in real time
  • You want conversation history with minimal code
  • You need per-message error states and retry
  • The UI maps naturally to a message list (sidebar history, branching, search)

Choose useCompletion when:

  • Building single-turn flows: summarization, translation, code generation, form fill
  • You don’t need message history or role semantics
  • You want a smaller dependency surface and simpler mental model
  • You’re streaming into a non-chat component (markdown preview, diff view, textarea)
  • You plan to parse the raw stream yourself for custom protocols

Choose neither (use useStream directly) when:

  • You need full control over the data stream protocol
  • You’re building a custom protocol (e.g., Server-Sent Events with non-standard events)
  • You want to multiplex multiple streams in one component
  • You’re integrating with a non-OpenAI-compatible streaming format

The Vercel AI SDK’s hook hierarchy is intentional: useStreamuseCompletionuseChat. Each layer adds opinionated state management. Start at the bottom and move up only when the next layer’s opinions match your product. Don’t fight useChat’s message schema if you don’t have messages; don’t reimplement useChat’s tool-call rendering if you do.

Tagsusechatusecompletionvercel-ai-sdkcomparison

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All vercel ai sdk streaming chat ui (usechat) posts →