React hooks for TanStack AI, providing convenient React bindings for the headless client. For React Native, the documented support surface is narrow: useChat with chat connection adapters. React DOM-specific UI packages and TanStack AI devtools UI are not part of the React Native support surface.
For a complete native journey, see Quick Start: React Native.
npm install @tanstack/ai-reactnpm install @tanstack/ai-reactMain hook for managing chat state in React with full type safety.
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
import {
clientTools,
createChatClientOptions,
type InferChatMessages
} from "@tanstack/ai-client";
function ChatComponent() {
// Create client tool implementations
const updateUI = updateUIDef.client((input) => {
setNotification(input.message);
return { success: true };
});
// Create typed tools array (no 'as const' needed!)
const tools = clientTools(updateUI);
const chatOptions = createChatClientOptions({
connection: fetchServerSentEvents("/api/chat"),
tools,
});
// Fully typed messages!
type ChatMessages = InferChatMessages<typeof chatOptions>;
const { messages, sendMessage, isLoading, error, addToolApprovalResponse } =
useChat(chatOptions);
return <div>{/* Chat UI with typed messages */}</div>;
}import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
import {
clientTools,
createChatClientOptions,
type InferChatMessages
} from "@tanstack/ai-client";
function ChatComponent() {
// Create client tool implementations
const updateUI = updateUIDef.client((input) => {
setNotification(input.message);
return { success: true };
});
// Create typed tools array (no 'as const' needed!)
const tools = clientTools(updateUI);
const chatOptions = createChatClientOptions({
connection: fetchServerSentEvents("/api/chat"),
tools,
});
// Fully typed messages!
type ChatMessages = InferChatMessages<typeof chatOptions>;
const { messages, sendMessage, isLoading, error, addToolApprovalResponse } =
useChat(chatOptions);
return <div>{/* Chat UI with typed messages */}</div>;
}Extends ChatClientOptions from @tanstack/ai-client:
Note: Client tools are now automatically executed - no onToolCall callback needed!
interface UseChatReturn {
messages: UIMessage[];
sendMessage: (content: string) => Promise<void>;
append: (message: ModelMessage | UIMessage) => Promise<void>;
addToolResult: (result: {
toolCallId: string;
tool: string;
output: any;
state?: "output-available" | "output-error";
errorText?: string;
}) => Promise<void>;
addToolApprovalResponse: (response: {
id: string;
approved: boolean;
}) => Promise<void>;
reload: () => Promise<void>;
stop: () => void;
isLoading: boolean;
error: Error | undefined;
setMessages: (messages: UIMessage[]) => void;
clear: () => void;
}interface UseChatReturn {
messages: UIMessage[];
sendMessage: (content: string) => Promise<void>;
append: (message: ModelMessage | UIMessage) => Promise<void>;
addToolResult: (result: {
toolCallId: string;
tool: string;
output: any;
state?: "output-available" | "output-error";
errorText?: string;
}) => Promise<void>;
addToolApprovalResponse: (response: {
id: string;
approved: boolean;
}) => Promise<void>;
reload: () => Promise<void>;
stop: () => void;
isLoading: boolean;
error: Error | undefined;
setMessages: (messages: UIMessage[]) => void;
clear: () => void;
}Re-exported from @tanstack/ai-client for convenience:
import {
fetchServerSentEvents,
fetchHttpStream,
xhrServerSentEvents,
xhrHttpStream,
stream,
type ConnectionAdapter,
type FetchConnectionOptions,
type XhrConnectionOptions,
} from "@tanstack/ai-react";import {
fetchServerSentEvents,
fetchHttpStream,
xhrServerSentEvents,
xhrHttpStream,
stream,
type ConnectionAdapter,
type FetchConnectionOptions,
type XhrConnectionOptions,
} from "@tanstack/ai-react";For React Native or Expo chat screens, use an absolute server URL and prefer xhrHttpStream() with a server route that returns toHttpResponse(). Use xhrServerSentEvents() with toServerSentEventsResponse() when you want SSE. Use fetchHttpStream() only when the runtime supports streaming fetch, Response.body.getReader(), and TextDecoder; otherwise it throws UnsupportedResponseStreamError.
XHR adapter options include headers, withCredentials, signal, body, and xhrFactory. Fetch adapter options include headers, credentials, signal, body, and fetchClient. Both option objects may be provided directly or as a function that resolves per request.
For error narrowing, import UnsupportedResponseStreamError and StreamTruncatedError from @tanstack/ai-client.
import { useState } from "react";
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
export function Chat() {
const [input, setInput] = useState("");
const { messages, sendMessage, isLoading } = useChat({
connection: fetchServerSentEvents("/api/chat"),
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (input.trim() && !isLoading) {
sendMessage(input);
setInput("");
}
};
return (
<div>
<div>
{messages.map((message) => (
<div key={message.id}>
<strong>{message.role}:</strong>
{message.parts.map((part, idx) => {
if (part.type === "thinking") {
return (
<div key={idx} className="text-sm text-gray-500 italic">
💠Thinking: {part.content}
</div>
);
}
if (part.type === "text") {
return <span key={idx}>{part.content}</span>;
}
return null;
})}
</div>
))}
</div>
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
disabled={isLoading}
/>
<button type="submit" disabled={isLoading}>
Send
</button>
</form>
</div>
);
}import { useState } from "react";
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
export function Chat() {
const [input, setInput] = useState("");
const { messages, sendMessage, isLoading } = useChat({
connection: fetchServerSentEvents("/api/chat"),
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (input.trim() && !isLoading) {
sendMessage(input);
setInput("");
}
};
return (
<div>
<div>
{messages.map((message) => (
<div key={message.id}>
<strong>{message.role}:</strong>
{message.parts.map((part, idx) => {
if (part.type === "thinking") {
return (
<div key={idx} className="text-sm text-gray-500 italic">
💠Thinking: {part.content}
</div>
);
}
if (part.type === "text") {
return <span key={idx}>{part.content}</span>;
}
return null;
})}
</div>
))}
</div>
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
disabled={isLoading}
/>
<button type="submit" disabled={isLoading}>
Send
</button>
</form>
</div>
);
}import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
export function ChatWithApproval() {
const { messages, sendMessage, addToolApprovalResponse } = useChat({
connection: fetchServerSentEvents("/api/chat"),
});
return (
<div>
{messages.map((message) =>
message.parts.map((part) => {
if (
part.type === "tool-call" &&
part.state === "approval-requested" &&
part.approval
) {
return (
<div key={part.id}>
<p>Approve: {part.name}</p>
<button
onClick={() =>
addToolApprovalResponse({
id: part.approval!.id,
approved: true,
})
}
>
Approve
</button>
<button
onClick={() =>
addToolApprovalResponse({
id: part.approval!.id,
approved: false,
})
}
>
Deny
</button>
</div>
);
}
return null;
})
)}
</div>
);
}import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
export function ChatWithApproval() {
const { messages, sendMessage, addToolApprovalResponse } = useChat({
connection: fetchServerSentEvents("/api/chat"),
});
return (
<div>
{messages.map((message) =>
message.parts.map((part) => {
if (
part.type === "tool-call" &&
part.state === "approval-requested" &&
part.approval
) {
return (
<div key={part.id}>
<p>Approve: {part.name}</p>
<button
onClick={() =>
addToolApprovalResponse({
id: part.approval!.id,
approved: true,
})
}
>
Approve
</button>
<button
onClick={() =>
addToolApprovalResponse({
id: part.approval!.id,
approved: false,
})
}
>
Deny
</button>
</div>
);
}
return null;
})
)}
</div>
);
}import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
import {
clientTools,
createChatClientOptions,
type InferChatMessages
} from "@tanstack/ai-client";
import { updateUIDef, saveToStorageDef } from "./tool-definitions";
import { useState } from "react";
export function ChatWithClientTools() {
const [notification, setNotification] = useState(null);
// Create client implementations
const updateUI = updateUIDef.client((input) => {
// ✅ input is fully typed!
setNotification({ message: input.message, type: input.type });
return { success: true };
});
const saveToStorage = saveToStorageDef.client((input) => {
localStorage.setItem(input.key, input.value);
return { saved: true };
});
// Create typed tools array (no 'as const' needed!)
const tools = clientTools(updateUI, saveToStorage);
const { messages, sendMessage } = useChat({
connection: fetchServerSentEvents("/api/chat"),
tools, // ✅ Automatic execution, full type safety
});
return (
<div>
{messages.map((message) =>
message.parts.map((part) => {
if (part.type === "tool-call" && part.name === "updateUI") {
// ✅ part.input and part.output are fully typed!
return <div>Tool executed: {part.name}</div>;
}
})
)}
</div>
);
}import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
import {
clientTools,
createChatClientOptions,
type InferChatMessages
} from "@tanstack/ai-client";
import { updateUIDef, saveToStorageDef } from "./tool-definitions";
import { useState } from "react";
export function ChatWithClientTools() {
const [notification, setNotification] = useState(null);
// Create client implementations
const updateUI = updateUIDef.client((input) => {
// ✅ input is fully typed!
setNotification({ message: input.message, type: input.type });
return { success: true };
});
const saveToStorage = saveToStorageDef.client((input) => {
localStorage.setItem(input.key, input.value);
return { saved: true };
});
// Create typed tools array (no 'as const' needed!)
const tools = clientTools(updateUI, saveToStorage);
const { messages, sendMessage } = useChat({
connection: fetchServerSentEvents("/api/chat"),
tools, // ✅ Automatic execution, full type safety
});
return (
<div>
{messages.map((message) =>
message.parts.map((part) => {
if (part.type === "tool-call" && part.name === "updateUI") {
// ✅ part.input and part.output are fully typed!
return <div>Tool executed: {part.name}</div>;
}
})
)}
</div>
);
}Helper to create typed chat options (re-exported from @tanstack/ai-client).
import {
clientTools,
createChatClientOptions,
type InferChatMessages
} from "@tanstack/ai-client";
// Create typed tools array (no 'as const' needed!)
const tools = clientTools(tool1, tool2);
const chatOptions = createChatClientOptions({
connection: fetchServerSentEvents("/api/chat"),
tools,
});
type Messages = InferChatMessages<typeof chatOptions>;import {
clientTools,
createChatClientOptions,
type InferChatMessages
} from "@tanstack/ai-client";
// Create typed tools array (no 'as const' needed!)
const tools = clientTools(tool1, tool2);
const chatOptions = createChatClientOptions({
connection: fetchServerSentEvents("/api/chat"),
tools,
});
type Messages = InferChatMessages<typeof chatOptions>;Re-exported from @tanstack/ai-client:
Re-exported from @tanstack/ai: