💪 Show the messages on the page
The previous exercise left the chat panel talking to
/api/chat through a hand-written fetch call. The answer comes back as raw chunks of text in a <pre> block. It works, but the panel does not feel like a chat: the conversation is not remembered between sends, the answer does not appear word by word, and there is no nice loading state.In this exercise you will replace that hand-written code with TanStack AI's
useChat hook. By the end the panel will feel like a real chat: messages appear word by word, the conversation is remembered, and the bot knows what you said two messages ago.🐨 What you will do
In
app/components/product-chat.tsx, the imports you need are already at the top of the file: useChat and fetchServerSentEvents from @tanstack/ai-react.-
🐨 Call
useChat({ connection: fetchServerSentEvents('/api/chat') })and readmessages,sendMessage, andisLoadingfrom what it returns. -
💣 Delete the four 💣-marked pieces that belong to the hand-written
fetch:- The
reply,error,isLoadinguseStatecalls - The
sendHandRolledfunction - The
<pre>{reply}</pre>and<div>⚠️ {error}</div>JSX inside the messages area - The
void useChat/void fetchServerSentEventslines (not needed once you actually use them)
- The
-
🐨 In the messages area, show the chat. Marty 💰 has left a ready-to-use JSX block as a comment inside
product-chat.tsx, right after the bomb-marked code you just deleted. Uncomment it. It walks each message'spartsand shows everytextpart. -
🐨 Change the form's
onSubmitso it callssendMessage(draft)and then clears the draft:<form onSubmit={(event) => { event.preventDefault() if (!draft.trim() || isLoading) return void sendMessage(draft) setDraft('') }} ... >
🦉 About useChat
useChat is the main React hook in TanStack AI. It takes care of the connection to the server, the list of messages, the loading flag, and everything else about the chat lifecycle. You do not write any of that yourself.The
messages array it gives you back is not shaped like the simple { role, content } pairs you send to the server. Each message is a UIMessage: { id, role, parts: Array<MessagePart> }. A MessagePart can be text, tool-call, tool-result, thinking, an image, audio, video, or a document. For now you will only see text parts. We will handle the others as they show up in later sections.📜 Docs
✅ How to know it is working
Open any product page, click the chat bubble, and have a short conversation.
- Before this exercise: sending a message shows raw chunks of text in the panel.
- After this exercise:
- Your message shows up right away as an amber bubble on the right.
- The bot's answer comes in word by word as a stone-colored bubble on the left.
- Send a follow-up message like "what did I just ask?" — the bot remembers the earlier message.
- In devtools → Network, there is one request per send, and the full chat so far is in the request body.
🚨 If you get stuck
- TypeScript shows a duplicate
isLoadingerror → you readisLoadingfromuseChatbut did not delete the olduseState<isLoading>line. Delete the old one. - The panel scrolls in a strange way or closes every time you send a message → keep the
isOpenstate local to the panel. Do not try to read it fromuseChat. - The bot does not remember the previous message → look in the Network tab. If only the latest message is in the request body, you may be building your own array and giving it to
sendMessage. PasssendMessage(draft)a plain string instead; the hook will add it to the messages list for you.