Deuz SDK
Modules

React Hooks

useChat and useObject — client-side hooks over the Deuz UI wire, with automatic client-tool round-trips and tool-approval pauses.

@deuz-sdk/core/react ships two hooks built on readDeuzStream: useChat for conversational UIs and useObject for streaming structured output. They are plain TypeScript hooks (no JSX, no components) and work with any React framework.

React is an optional peer (^18 || ^19) — install it yourself; nothing else in the SDK depends on it, and no other subpath pulls it in. The hooks are SSR-safe: network runs only inside user-triggered callbacks, never at render time.

npm i react

useChat

import { useChat } from '@deuz-sdk/core/react';

const {
  messages,            // UIMessage[] — render-friendly
  status,              // 'idle' | 'streaming' | 'error'
  error,               // Error | undefined
  sendMessage,         // (text: string) => Promise<void>
  regenerate,          // rerun the last user turn
  stop,                // abort the in-flight stream (not an error)
  pendingApprovals,    // ToolApprovalRequest[] — chat is PAUSED while non-empty
  addToolApprovalResponse, // ({ approvalId, approved, reason? }) => Promise<void>
} = useChat({
  api: '/api/chat',    // endpoint serving toDeuzStreamResponse
  onToolCall: async (call) => runInBrowser(call), // client tools (optional)
});

Options

OptionTypeNotes
apistringRoute that returns toDeuzStreamResponse output.
initialMessagesMessage[]Seed the canonical history (e.g. restored from storage).
headers / bodyrecordsMerged into every request (body fields ride next to messages).
onToolCall(call) => unknown | Promise<unknown>Executor for client tools — see below.
onError(error) => voidCalled when the stream fails (state also carries the error).
fetchtypeof fetchInjectable transport (tests, custom auth).

What it renders vs what it sends

The hook keeps two histories. messages is the render-friendly UIMessage[] (accumulated content, reasoning, and toolCalls with live state). Internally it maintains the canonical Message[] — reconstructing each assistant tool_use turn from the streamed tool-call parts exactly like the manual browser recipe — and POSTs { messages, ...body } on every round. History is immutable (new arrays every update).

Client tools — automatic round-trip

When the server streams a tool-call it did not execute (a client tool), the hook calls your onToolCall, appends the tool_result to the canonical history, and re-POSTs automatically — looping until a round has no pending client calls. A throwing onToolCall self-heals as an is_error result, mirroring the server loop's behavior.

Tool approvals — pause and resume

A tool-approval-request part pauses the chat: the request lands in pendingApprovals, the matching toolCalls entry flips to state: 'approval-requested', and nothing is re-POSTed. Record verdicts with addToolApprovalResponse; once every pending approval has one, the hook resumes with approvalResponses in the request body — the server settles the gated calls, the client never fabricates gated tool_results.

{pendingApprovals.map((req) => (
  <ApprovalCard
    key={req.approvalId}
    request={req}
    onDecision={(approved, reason) =>
      addToolApprovalResponse({ approvalId: req.approvalId, approved, reason })
    }
  />
))}

useObject

Streams streamObject partials from a route that returns toDeuzObjectStreamResponse:

import { useObject } from '@deuz-sdk/core/react';

const { object, isLoading, error, submit, stop } = useObject<Recipe>({ api: '/api/recipe' });

// submit POSTs { input } and streams object-delta parts into `object`:
await submit({ dish: 'menemen' });
// object: DeepPartial<Recipe> | undefined — every field optional at every depth

Each object-delta replaces object wholesale (no merging needed). String fields stream truncated ('mene''menemen'), so render defensively. Wire error parts and rejected fetches land in error; stop() aborts without erroring.

Server side:

app/api/recipe/route.ts
import { streamObject } from '@deuz-sdk/core';
import { toDeuzObjectStreamResponse } from '@deuz-sdk/core/ui';

export async function POST(req: Request) {
  const { input } = await req.json();
  const result = streamObject({ model, schema: recipeSchema, messages: toMessages(input) });
  return toDeuzObjectStreamResponse(result);
}

See also

On this page