useFrontendTool
React hook for registering client-side tool handlers with optional UI rendering in React Native
Open your coding agent in your project's folder, or in an empty folder for a new app.This runs in a coding agent on your computer.
Overview
useFrontendTool registers a client-side tool with CopilotKit at component scope. When the agent decides to call the tool, the provided handler function executes on the device. Optionally, you can supply a render component to display custom UI in the chat showing the tool's execution progress and results.
Re-exported from @copilotkit/react-core/v2. It is identical to the React (V2) useFrontendTool; only the import path differs.
The hook manages the full registration lifecycle: it warns if a tool with the same name already exists, registers the tool and its render component on mount, and cleans up both registrations on unmount. In v2, parameter schemas are defined using Zod instead of plain parameter arrays.
Signature
import { useFrontendTool } from "@copilotkit/react-native";
function useFrontendTool<T extends Record<string, unknown>>(
tool: ReactFrontendTool<T>,
deps?: ReadonlyArray<unknown>,
): void;Parameters
Prop
Type
Prop
Type
Usage
Basic Tool with Zod Parameters
import { useState } from "react";
import { Text, View } from "react-native";
import { useFrontendTool } from "@copilotkit/react-native";
import { z } from "zod";
function TodoManager() {
const [todos, setTodos] = useState<string[]>([]);
useFrontendTool(
{
name: "addTodo",
description: "Add a new item to the user's todo list",
parameters: z.object({
text: z.string().describe("The todo item text"),
priority: z.enum(["low", "medium", "high"]).describe("Priority level"),
}),
handler: async ({ text, priority }) => {
setTodos((prev) => [...prev, text]);
return `Added "${text}" with ${priority} priority`;
},
},
[],
);
return (
<View>
{todos.map((t, i) => (
<Text key={i}>{t}</Text>
))}
</View>
);
}Tool with Custom Render Component
import { Text, View } from "react-native";
import { useFrontendTool, ToolCallStatus } from "@copilotkit/react-native";
import { z } from "zod";
function WeatherWidget() {
useFrontendTool(
{
name: "getWeather",
description: "Fetch and display weather information for a city",
parameters: z.object({
city: z.string().describe("City name"),
units: z.enum(["celsius", "fahrenheit"]).default("celsius"),
}),
handler: async ({ city, units }, { signal }) => {
const response = await fetch(
`https://your-server/api/weather?city=${city}&units=${units}`,
{ signal },
);
const data = await response.json();
return JSON.stringify(data);
},
render: ({ args, status, result }) => {
if (status === ToolCallStatus.InProgress) {
return (
<View>
<Text>Fetching weather for {args.city}...</Text>
</View>
);
}
if (status === ToolCallStatus.Complete && result) {
const data = JSON.parse(result);
return (
<View>
<Text>{data.city}</Text>
<Text>
{data.temperature}° {data.units}
</Text>
<Text>{data.conditions}</Text>
</View>
);
}
return null;
},
},
[],
);
return null;
}Conditionally Available Tool
import { View } from "react-native";
import { useFrontendTool } from "@copilotkit/react-native";
import { z } from "zod";
function AdminPanel({ isAdmin }: { isAdmin: boolean }) {
useFrontendTool(
{
name: "deleteUser",
description: "Delete a user account by ID (admin only)",
parameters: z.object({
userId: z.string().describe("The ID of the user to delete"),
}),
handler: async ({ userId }) => {
await fetch(`https://your-server/api/users/${userId}`, {
method: "DELETE",
});
return `User ${userId} deleted`;
},
available: isAdmin ? "enabled" : "disabled",
},
[isAdmin],
);
return <View>{/* admin UI */}</View>;
}The render type React Native needs
render is typed as ReactToolCallRenderer<T>["render"], which is a
React.ComponentType. A function component's return type is ReactNode, so
this compiles:
useFrontendTool({
name: "showWeather",
description: "Show the weather",
parameters: z.object({ city: z.string() }),
handler: async ({ city }) => city,
// Compiles. `ReactNode` includes `string`.
render: ({ args }) => `Weather in ${args.city}`,
});and then fails on a device. The string reaches the prebuilt chat's FlatList as
a bare text child of a host view, and React Native throws Text strings must be
rendered within a <Text> component. The same applies to a number, an array of
elements, or undefined — all of them are ReactNode, and none of them is
something React Native's list renderer can draw.
@copilotkit/react-native exports an opt-in type that narrows the return to
ReactElement | null, so the compiler rejects it instead:
import { useFrontendTool } from "@copilotkit/react-native";
import type { FrontendToolRenderFunction } from "@copilotkit/react-native";
import { Text } from "react-native";
import { z } from "zod";
const renderWeather: FrontendToolRenderFunction<{ city: string }> = ({
args,
}) => <Text>{args.city}</Text>;
// `TS2322: Type 'string' is not assignable to type 'ReactElement…'`
const broken: FrontendToolRenderFunction<{ city: string }> = ({ args }) =>
`Weather in ${args.city}`;
useFrontendTool({
name: "showWeather",
description: "Show the weather",
parameters: z.object({ city: z.string() }),
handler: async ({ city }) => city,
render: renderWeather,
});Prop
Type
It is opt-in, and only opt-in
This type changes no hook signature. A renderer written inline in a
useFrontendTool call is still checked against react-core's
ReactNode-returning contract, and a bare string still compiles there. You get
the narrowing on renderers you annotate with it — declare render as a
standalone FrontendToolRenderFunction<T> and pass it in, as above. It does
not make the package safe on its own.
useRenderTool needs none of
this: it declares its own render as (props) => ReactElement | null, so a bare
string is rejected there without any annotation.
Behavior
- Duplicate detection: If a tool with the same
nameis already registered, the hook logs a warning. Only one tool per name is active at a time. - Mount/Unmount lifecycle: The tool and its optional render component are registered on mount and removed on unmount.
- Dependency tracking: When
depsis provided, the registration is refreshed whenever a dependency changes. Plain data compares by value; functions and other objects compare by identity. See Dependency comparison below. - Handler and render are captured at registration: Neither closure is re-read on every render. A value your
handlerorrenderreads from component state or props is frozen at whatever it was when the tool was last registered, so it must be declared indeps. - Render component lifecycle: If a
renderfunction is provided, it is added to the internal render tool calls registry. It receives streamingargs(partial duringInProgress, complete duringExecutingandComplete). render's return type isReactNode, notReactElement | null: a bare string typechecks and then throws in the chat'sFlatList. Annotate the renderer withFrontendToolRenderFunction<T>to have the compiler reject it.- No return value: The hook returns
void.
Dependency comparison
The hook reduces deps to a version number and puts only that number in its
registration effect (packages/react-core/src/v2/hooks/tool-registration.ts).
deps is never serialized. Each entry is compared with the previous render's:
- Plain data compares by value: strings, numbers, booleans,
null,undefined, dates, and plain objects and arrays of those. An inline literal such as[{ status }]or[items.filter(isOpen)]re-registers only when its contents change. Key order does not matter. - A function never counts as changed, as before: a new callback in
depsdoes not re-register the tool. Read a changing callback through a ref (below). - Class instances,
Map,Setand other non-plain objects compare by identity. - A circular value, such as the
agentreturned byuseAgent, is safe to list. A cycle inside plain data is safe too. - Adding or removing an entry counts as a change.
To keep the registration stable while the closure always sees the current
value, keep the value out of deps and read it through a latest-value ref:
import { useEffect, useRef } from "react";
import { useFrontendTool } from "@copilotkit/react-native";
import { z } from "zod";
function DraftTool({ onSubmit }: { onSubmit: () => Promise<string> }) {
const latestOnSubmit = useRef(onSubmit);
useEffect(() => {
latestOnSubmit.current = onSubmit;
});
useFrontendTool(
{
name: "submitDraft",
description: "Submit the current draft",
parameters: z.object({}),
handler: async () => latestOnSubmit.current(),
},
[],
);
return null;
}The same ref pattern applies to a render component that needs live state.
Related
useRenderTool: register renderer-only tool call UI (named or wildcard), and the migration notes for React Native's older local hook of the same nameuseCopilotKit: access the core instance and run tools programmatically- React (V2) reference: the web equivalent this page mirrors