useFrontendTools
React hook for registering a list of client-side tools whose length can change between renders
Overview
useFrontendTools registers many client-side tools in one call. Use it when the set of tools is not known when you write the component: tools built from state, from props, or from a list the backend returned.
useFrontendTool registers exactly one tool per call, so it cannot be called in a loop over a list whose length changes. That breaks the rules of hooks. useFrontendTools runs a single effect over the whole array instead, so the array may be empty on one render and hold twenty entries on the next.
Each entry is the same ReactFrontendTool object that useFrontendTool takes. See that page for the full tool shape, including parameters, handler, render, and webmcp.
Signature
import { useFrontendTools } from "@copilotkit/react-core/v2";
function useFrontendTools(
tools: ReadonlyArray<ReactFrontendTool<any>> | undefined,
deps?: ReadonlyArray<unknown>,
): void;Parameters
Prop
Type
Prop
Type
Usage
Tools built from application state
function ReportTools({ reports }: { reports: Report[] }) {
const navigate = useNavigate();
useFrontendTools(
reports.map((report) => ({
name: `open_${report.id}`,
description: `Open the ${report.title} report`,
handler: async () => {
navigate(`/reports/${report.id}`);
return `Opened ${report.title}`;
},
})),
[navigate],
);
return null;
}When reports grows, the new tools are registered. When a report disappears from the list, its tool is unregistered and the rest stay registered.
Tools described by the backend
function BackendTools() {
const { data, isLoading } = useQuery({
queryKey: ["tool-descriptors"],
queryFn: fetchToolDescriptors,
});
useFrontendTools(
data?.map((descriptor) => ({
name: descriptor.name,
description: descriptor.description,
parameters: z.object({
query: z.string().describe("What to look up"),
}),
handler: async ({ query }) => {
const result = await runDescriptor(descriptor.id, query);
return JSON.stringify(result);
},
})),
[],
);
return isLoading ? <Spinner /> : null;
}Passing undefined while the query is in flight registers nothing. The tools appear once the data arrives, without a second code path for the loading state.
Turning a subset of tools off
function PermissionedTools({ isAdmin }: { isAdmin: boolean }) {
useFrontendTools([
{
name: "searchOrders",
description: "Search orders by status",
parameters: z.object({ status: z.string() }),
handler: async ({ status }) => JSON.stringify(await search(status)),
},
{
name: "refundOrder",
description: "Refund an order by ID (admin only)",
parameters: z.object({ orderId: z.string() }),
handler: async ({ orderId }) => refund(orderId),
available: isAdmin,
},
]);
return null;
}available: false keeps the tool registered but hides it from the agent. Toggling it re-registers that tool, the same as with useFrontendTool.
Tools with render components
function ChartTools({ metrics }: { metrics: Metric[] }) {
useFrontendTools(
metrics.map((metric) => ({
name: `chart_${metric.key}`,
description: `Chart the ${metric.label} metric`,
parameters: z.object({ days: z.number() }),
handler: async ({ days }) => JSON.stringify(await load(metric, days)),
render: ({ status, result }) => {
if (status !== ToolCallStatus.Complete || !result) {
return <div className="animate-pulse">Loading {metric.label}…</div>;
}
return <Chart data={JSON.parse(result)} />;
},
})),
[],
);
return null;
}Behavior
- Value-based re-registration: The array is normally built inline, so its identity changes on every render. The hook keys on the
name,description,agentId,available, andwebmcpof every entry instead. A re-render that produces an equal list does not re-register anything. - Descriptions stay current:
descriptionis part of the key. A description derived from your data, such as`Open the ${report.title} report`, reaches the agent as soon as the data changes, with nodepsentry. This is the one place the key differs fromuseFrontendTool, which keys only onname,available, andwebmcp. - Adding and removing: When the list changes, tools that are new get registered and tools that left get unregistered. Tools present in both the old and new list are re-registered with their latest definition.
- Unmount: Every tool the hook registered is unregistered on unmount.
- Duplicate names: A
nameandagentIdpair listed twice in one array is a mistake in the calling code. The last entry wins and the hook logs a warning naming the tool. - Overriding another registration: If a tool with the same
nameandagentIdwas already registered elsewhere, the hook overrides it and logs a warning, matchinguseFrontendTool. - Render component lifecycle: Entries with a
renderfunction register a renderer. Renderers are deliberately left in place after unmount so that past tool calls still render in the chat history. - Handlers and parameters are not part of the key: A
handleris a function and aparametersschema is an object, so neither can be compared by value. Changing only one of them does not re-register on its own. Passdepswhen a handler closes over a value that must stay current, as the examples above do withnavigate. - No return value: The hook returns
void.
Related
useFrontendTool-- register a single tool; the reference for the tool shapeuseHumanInTheLoop-- for tools that pause execution and wait for user inputuseRenderTool-- register renderer-only tool call UI (named or wildcard)