Chapter 7: Frontend AI Hooks - useChat and useCompletion
Theoretical Foundations
In the previous chapter, we explored how to construct robust, type-safe AI backends using LangChain.js and Zod. We focused on the server-side orchestration of AI models, ensuring that the data flowing out of our API endpoints is predictable and validated. Now, we shift our focus to the client side—the user's browser. The fundamental challenge here is bridging the gap between a static user interface and a dynamic, streaming, and often non-deterministic AI model. This is where the Vercel AI SDK's hooks, useChat and useCompletion, become indispensable.
At its heart, this chapter is about managing state and streaming. A traditional web request is a simple transaction: the client sends a request, waits, and receives a complete response. It is synchronous and stateless. AI interactions, however, are inherently different. They are often conversational (requiring context) and can produce responses over time (streaming) to improve perceived performance. Manually managing this complexity in React—tracking message history, handling streaming chunks, managing loading states, and updating the UI in real-time—is error-prone and tedious. The Vercel AI SDK abstracts this entire process, providing a declarative way to interact with AI models directly from your components.
The Analogy: A Live News Broadcast vs. a Printed Newspaper
To understand the necessity of these hooks, consider the difference between a printed newspaper and a live news broadcast.
-
The Printed Newspaper (Traditional API Calls): This is like a standard
fetchrequest. You send a query (the order for the paper), you wait for it to be delivered, and you receive the entire, finalized product at once. The information is static and complete. If you want a new piece of information, you must place a new order. This is simple, but it lacks immediacy and interactivity. -
The Live News Broadcast (Streaming with
useChat): This is analogous to howuseChatand streaming work. The anchor (the AI model) begins speaking, and you receive the information word by word, sentence by sentence, in real-time. You don't have to wait for the entire segment to be over to start understanding the point. The broadcast maintains a sense of context (the conversation so far) and allows for new segments to be added dynamically. The Vercel AI SDK's hooks are the production studio equipment that manages this live feed, ensuring the broadcast is smooth, synchronized, and handles interruptions or new inputs gracefully.
The useChat Hook: The Conversational Orchestrator
The useChat hook is the primary tool for building multi-turn conversational interfaces. It is designed to manage the entire lifecycle of a chat interaction, from the user's first message to the final streamed response.
What It Does: State Management and Streaming
At its core, useChat is a sophisticated state management hook. When you call it, it returns an object containing several key properties and functions:
messages: An array representing the complete conversation history. Each message object typically contains arole('user'or'assistant') andcontent. This is the single source of truth for the chat UI.input: A string representing the current value of the user's input field.handleInputChange: A function to update theinputstate as the user types.appendorhandleSubmit: Functions to send the user's message to the API endpoint. Theappendfunction allows you to manually add a message to the history and trigger an API call, whilehandleSubmitis typically used to wrap an HTML form submission.isLoading: A boolean flag that istruewhile the AI is generating a response, allowing you to show loading indicators (e.g., a typing animation).stop: A function to manually abort a streaming response in progress.
Under the hood, useChat handles a complex sequence of events:
1. When a user submits a message, useChat appends the user's message to the messages array and immediately updates the UI.
2. It then makes a POST request to your specified API endpoint. Crucially, it expects the response to be a ReadableStream (specifically, a stream of Server-Sent Events or SSE).
3. As chunks of text arrive from the stream, useChat progressively updates the messages array, appending the new content to the assistant's message. This triggers a re-render in your React component, creating the real-time "typing" effect.
4. Once the stream is complete, the final assistant message is fully populated in the history, and isLoading is set back to false.
Why It's Necessary: The State Machine for Conversation
Without useChat, you would need to build a custom state machine. You would have to:
* Manually create a useState for the message history array.
* Create another useState for the user's input field.
* Create a useState for a loading boolean.
* Write a useEffect or an event handler to manage the fetch call.
* Use a TransformStream or a while loop to read the response body chunk by chunk.
* Manually update the message state with each chunk.
* Handle errors, race conditions (e.g., a user submitting a new message while a response is still streaming), and cleanup.
useChat encapsulates all of this logic into a single, declarative hook. It provides a standardized, battle-tested solution that handles the nuances of streaming, state synchronization, and user interaction, allowing you to focus on the UI rather than the complex plumbing of real-time data flow.
The useCompletion Hook: The Focused Text Generator
While useCompletion shares similarities with useChat in its streaming capabilities, its purpose is more focused. It is not designed for conversational state but for single-turn text generation tasks.
What It Does: Streamlined Single-Output Generation
useCompletion is ideal for applications where you need to generate text based on a single prompt, without the need for a back-and-forth history. Examples include:
* A summarization tool where the user pastes text and gets a concise summary.
* A translation service.
* A creative writing prompt generator.
* A code generation tool for a specific function.
The API is simpler than useChat. It returns:
* completion: The current generated text string, which is updated as the stream arrives.
* input: The prompt string.
* handleInputChange: To update the prompt.
* handleSubmit: To trigger the generation.
* isLoading: A loading state.
* stop: An abort function.
The key difference is the absence of a messages array. The state is managed as a single string (completion) rather than a structured history. It's a one-shot, stateful generator.
Why It's Necessary: Specialization for Simplicity
You might ask, "Couldn't I just use useChat for this?" Technically, yes, but useCompletion provides a more semantically correct and lightweight API for single-turn tasks. Using useChat for a summarizer would mean managing a message history array for a single prompt-response pair, which is unnecessary overhead. useCompletion strips away the conversational complexity, offering a direct and efficient way to handle non-dialogue-based text generation. It's the difference between using a full-featured conversation framework for a simple FAQ bot versus a dedicated, lightweight tool for a specific task.
The Analogy: A Command-Line Terminal vs. a Chat Room
To further clarify the distinction between the two hooks, think of them as different user interfaces for AI interaction.
-
useChatis like a Chat Room (e.g., Slack or Discord): The context is persistent. What was said five minutes ago is still relevant and influences the current conversation. The state is a log of all participants' contributions. You need to maintain this entire history to understand the flow. This is perfect for customer support bots, virtual companions, or any multi-turn dialogue. -
useCompletionis like a Command-Line Terminal (e.g., Bash or PowerShell): You enter a command (a prompt), and you get a result. The terminal doesn't remember your previous commands unless you explicitly script it to. Each command is largely independent. This is perfect for one-off tasks like "summarize this text," "translate this sentence," or "write a poem about X."
Visualizing the Data Flow
The following diagram illustrates the lifecycle of a user interaction using useChat. It highlights the client-side state management and the streaming communication with the backend API, which we previously built using LangChain.js and Zod.
Explicit Reference to Previous Chapter: Zod for Client-Side Validation
In Chapter 6, we extensively used Zod to validate the output of our LangChain.js chains on the server. This ensured that our backend always returned a predictable, type-safe JSON object, preventing malformed data from ever reaching the client.
This principle is just as critical on the frontend. While useChat and useCompletion are primarily designed to stream raw text, the Vercel AI SDK provides a powerful mechanism to integrate Zod for client-side validation of structured responses. When you expect your AI model to return a structured object (e.g., a JSON object with summary and sentiment fields), you can define a Zod schema and pass it to the hook.
The SDK will then:
1. Receive the streamed text from the API.
2. Attempt to parse the accumulated text against the Zod schema in real-time.
3. Provide a parseError or the successfully parsed data object.
This brings the same robustness we built into our backend directly to the client, creating a end-to-end type-safe pipeline. It prevents the UI from trying to render an incomplete or malformed object, which could lead to runtime errors. This is a perfect example of how the principles of robust software engineering (like schema validation) are applied consistently across the full-stack AI application.
Basic Code Example
The useChat hook from the Vercel AI SDK abstracts away the complexity of managing WebSocket connections or Server-Sent Events (SSE) for real-time AI interactions. It handles the lifecycle of a conversation: sending user messages, receiving streamed tokens from the backend, updating the UI instantly, and managing loading states.
In this "Hello World" example, we build a minimal SaaS-style chat interface. We assume the frontend is a Next.js/React application calling a backend route (e.g., /api/chat) that utilizes LangChain.js and OpenAI.
Visualizing the Data Flow
The following diagram illustrates how the useChat hook bridges the user interface and the backend AI services.
Implementation: A Minimalist Chat Component
This component is fully self-contained. It uses the useChat hook to manage the conversation state and renders a simple list of messages.
// File: components/MinimalistChat.tsx
'use client'; // Required for Next.js App Router
import { useChat } from 'ai/react';
import { z } from 'zod';
/**
* @description A minimalist chat interface demonstrating the useChat hook.
* It handles user input, message streaming, and basic error states.
*/
export default function MinimalistChat() {
// 1. HOOK INITIALIZATION
// The useChat hook automatically manages:
// - `messages`: Array of message objects (role, content)
// - `input`: String value for the controlled input field
// - `handleInputChange`: Updates the input state
// - `handleSubmit`: Submits the message to the API
// - `isLoading`: Boolean indicating if the AI is processing
// - `error`: Error object if the API call fails
const { messages, input, handleInputChange, handleSubmit, isLoading, error } = useChat({
api: '/api/chat', // The backend endpoint handling the AI logic
});
// 2. CLIENT-SIDE RESPONSE VALIDATION (Zod)
// While useChat handles streaming, we can validate specific metadata
// if the backend sends structured JSON alongside text.
// This is a placeholder for where you would parse complex JSON streams.
const metadataSchema = z.object({
model: z.string(),
finishReason: z.string().optional(),
});
return (
<div className="flex flex-col w-full max-w-md mx-auto p-4 border rounded-lg shadow-sm">
{/* 3. MESSAGE DISPLAY AREA */}
<div className="flex flex-col gap-2 mb-4 h-64 overflow-y-auto">
{messages.length > 0 ? (
messages.map((message) => (
<div
key={message.id}
className={`p-3 rounded-md text-sm ${
message.role === 'user'
? 'bg-blue-100 text-blue-900 self-end'
: 'bg-gray-100 text-gray-900 self-start'
}`}
>
<strong>{message.role === 'user' ? 'You' : 'AI'}:</strong>
<span className="ml-2">{message.content}</span>
</div>
))
) : (
<div className="text-center text-gray-500 mt-10">
No messages yet. Start a conversation!
</div>
)}
{/* 4. LOADING STATE */}
{isLoading && (
<div className="text-center text-gray-400 animate-pulse">
Thinking...
</div>
)}
{/* 5. ERROR STATE */}
{error && (
<div className="bg-red-50 text-red-600 p-2 rounded text-xs">
Error: {error.message}
</div>
)}
</div>
{/* 6. INPUT FORM */}
<form onSubmit={handleSubmit} className="flex gap-2">
<input
type="text"
value={input}
onChange={handleInputChange}
placeholder="Type a message..."
className="flex-1 border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
disabled={isLoading} // Prevent input while AI is thinking
/>
<button
type="submit"
disabled={isLoading || !input.trim()}
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
Send
</button>
</form>
</div>
);
}
Detailed Line-by-Line Explanation
Here is the breakdown of the logic, numbered by logical blocks.
1. Hook Initialization
const { messages, input, handleInputChange, handleSubmit, isLoading, error } = useChat({
api: '/api/chat',
});
useChat(): This is the entry point. It initializes a local state machine for the chat.
* api: '/api/chat': We explicitly tell the hook which endpoint to call. If omitted, it defaults to /api/chat. This is crucial for routing in monorepos or when the backend logic is separated.
* Destructured State:
* messages: An array of objects { id: string, role: 'user' | 'assistant', content: string }. The hook automatically appends the AI's response here as it streams in.
* input: A string bound to the <input> element.
* handleSubmit: A function that prevents the default form submission, adds the user message to the messages array, and initiates the fetch request to the backend.
* isLoading: Specifically tracks the network request state, distinct from the message content streaming. It becomes true immediately on submit and false when the stream closes.
2. Client-Side Validation (Zod Integration)
* Context: WhileuseChat handles raw text streaming, real-world SaaS apps often receive structured JSON data (e.g., usage statistics, tool calls) inside the stream or at the end.
* Zod: We define a schema here. In a more advanced implementation, you would use the onFinish callback of useChat to parse message.content if it contains JSON, validating it against this schema to ensure data integrity before displaying complex UI elements.
3. Message Display Area
* Iteration: We map over themessages array.
* Styling: We conditionally apply classes based on message.role. This visual distinction is standard in chat UIs (bubbles on the right for users, left for AI).
* Streaming Behavior: Because useChat uses streaming, the AI message appears empty at first, then characters append to message.content incrementally. React re-renders this list on every token received.
4. Loading & Error States
*isLoading: Provides immediate feedback. Without this, the user might think the app is frozen during network latency.
* error: The useChat hook catches network errors or API errors (e.g., 500 Internal Server Error) and exposes them here. This prevents the app from crashing and allows for graceful degradation.
5. The Input Form
<form onSubmit={handleSubmit}>
<input onChange={handleInputChange} ... />
<button type="submit">Send</button>
</form>
value={input} and onChange={handleInputChange} create a controlled component. handleInputChange is a helper provided by the hook that updates the local state.
* Submit Logic: When the user clicks "Send" or hits Enter:
1. handleSubmit is invoked.
2. The user message is immediately added to messages (optimistic UI update).
3. A POST request is sent to /api/chat.
4. The UI switches to the loading state.
Common Pitfalls
When implementing useChat in a production SaaS environment, watch out for these specific JavaScript/TypeScript issues:
-
Vercel/Server Timeouts (The 10s Wall)
- Issue: AI generation can be slow. Standard serverless functions (like Vercel Edge) often have strict timeouts (e.g., 10 seconds for Hobby plans).
- Symptom: The stream cuts off abruptly, or the request fails with a 504 timeout.
- Solution: Ensure your backend (LangChain/OpenAI) is configured to stream tokens immediately. Do not wait for the full response to generate before sending it. Use
streamEventsin LangChain or standard OpenAI streaming parameters.
-
Missing
'use client'Directive- Issue: In Next.js 13+ App Router, components using hooks like
useChat(which rely onuseEffectanduseState) must be Client Components. - Symptom:
ReferenceError: window is not definedoruseChat is not a function. - Solution: Always add
'use client';at the very top of the file.
- Issue: In Next.js 13+ App Router, components using hooks like
-
Async/Await Loops in the Backend
- Issue: Developers often try to
awaitthe full result from OpenAI in the backend route before responding to the frontend. - Symptom: The frontend "spins" indefinitely with no data until the backend finishes processing, defeating the purpose of streaming.
- Solution: The backend must return a
ReadableStream. Do not useawaiton the final result; iterate over the stream chunks and write them to the response immediately.
- Issue: Developers often try to
-
Hallucinated JSON in Text Streams
- Issue: If you ask the LLM to return JSON, it might stream valid JSON initially but append a trailing explanation (e.g.,
{"key": "value"} Here is the data...). - Symptom:
JSON.parse()fails on the frontend when trying to process the message content. - Solution: Do not parse the stream incrementally as JSON. Wait for the
onFinishcallback (which fires when the stream ends) to parse the complete message content, or use a specific tool call mechanism provided by LangChain/OpenAI that separates structured data from text.
- Issue: If you ask the LLM to return JSON, it might stream valid JSON initially but append a trailing explanation (e.g.,
The chapter continues with advanced code, exercises and solutions with analysis, you can find them on the ebook on Leanpub.com or Amazon
Loading knowledge check...
Code License: All code examples are released under the MIT License. Github repo.
Content Copyright: Copyright © 2026 Edgar Milvus | Privacy & Cookie Policy. All rights reserved.
All textual explanations, original diagrams, and illustrations are the intellectual property of the author. To support the maintenance of this site via AdSense, please read this content exclusively online. Copying, redistribution, or reproduction is strictly prohibited.