Back to Copilotkit

useFrontendTool

showcase/shell-docs/src/content/reference/react-native/hooks/useFrontendTool.mdx

1.68.112.9 KB
Original Source

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.

<Callout type="info"> Re-exported from `@copilotkit/react-core/v2`. It is identical to the [React (V2) `useFrontendTool`](/reference/v2/hooks/useFrontendTool); only the import path differs. </Callout>

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

tsx
import { useFrontendTool } from "@copilotkit/react-native";

function useFrontendTool<T extends Record<string, unknown>>(
  tool: ReactFrontendTool<T>,
  deps?: ReadonlyArray<unknown>,
): void;

Parameters

<PropertyReference name="tool" type="ReactFrontendTool<T>" required> The tool definition object. <PropertyReference name="name" type="string" required> A unique name for the tool. The agent references this name when deciding to call the tool. If a tool with this name is already registered, a warning is logged. </PropertyReference> <PropertyReference name="description" type="string" required> A natural-language description that tells the agent what the tool does and when to use it. </PropertyReference> <PropertyReference name="parameters" type="z.ZodSchema" required> A Zod schema defining the tool's input parameters. The schema is used for both validation and type inference. </PropertyReference>

<PropertyReference name="handler" type="(args: T, context?: { toolCall, agent, signal? }) => Promise<string>" required

An async function that executes when the agent calls the tool. Receives the validated, typed arguments and an optional context object containing toolCall (the raw tool call metadata), agent (the agent instance that invoked the tool), and signal (an AbortSignal that is aborted when the user stops the agent via stopAgent() or agent.abortRun()). Long-running handlers can check signal.aborted to exit early. </PropertyReference>

<PropertyReference name="render" type="React.ComponentType<{ name: string; args: Partial<T>; status: ToolCallStatus; result: string | undefined }>"

An optional component rendered in the chat interface to visualize tool execution. In React Native the render component returns React Native elements (e.g. <View> / <Text>). The component receives name (the tool name), args (the arguments, partial while streaming and complete once execution starts), status (one of ToolCallStatus.InProgress, ToolCallStatus.Executing, or ToolCallStatus.Complete), and result (the string result returned by the handler, available only when status is Complete). </PropertyReference>

<PropertyReference name="available" type='"enabled" | "disabled" | "remote"' default='"enabled"'> Controls tool availability. Set to `"disabled"` to temporarily prevent the agent from calling the tool, or `"remote"` to indicate the tool is handled server-side. </PropertyReference> </PropertyReference> <PropertyReference name="deps" type="ReadonlyArray<unknown>"> An optional dependency array. When it changes, the tool is unregistered and re-registered so the new `handler` and `render` closures take effect. Pass every value your `handler` or `render` captures and that changes over time — both are captured at registration, not re-read on every render.

Despite the shape, this is not compared the way useEffect compares dependencies. useEffect compares each element with Object.is; this hook serialises the whole array with JSON.stringify and compares the resulting string. Only JSON-representable values compare reliably — a function, a Map, a Set or a #private-field class instance is invisible to it, and a circular value throws during render. See Dependency comparison for the exact rules and the two patterns that do work. </PropertyReference>

Usage

Basic Tool with Zod Parameters

tsx
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

tsx
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}&deg; {data.units}
              </Text>
              <Text>{data.conditions}</Text>
            </View>
          );
        }
        return null;
      },
    },
    [],
  );

  return null;
}

Conditionally Available Tool

tsx
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></View>;
}

Behavior

  • Duplicate detection: If a tool with the same name is 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 deps is provided, the registration is refreshed whenever the serialised array changes — JSON.stringify(deps), not useEffect's per-element Object.is. See Dependency comparison below; the difference is load-bearing.
  • Handler and render are captured at registration: Neither closure is re-read on every render. A value your handler or render reads from component state or props is frozen at whatever it was when the tool was last registered, so it must be declared in deps (subject to the comparison rules below).
  • Render component lifecycle: If a render function is provided, it is added to the internal render tool calls registry. It receives streaming args (partial during InProgress, complete during Executing and Complete).
  • No return value: The hook returns void.

Dependency comparison

The registration effect's real dependency list is [tool.name, tool.available, copilotkit, JSON.stringify(extraDeps)] (packages/react-core/src/v2/hooks/use-frontend-tool.tsx:45, where extraDeps is your deps ?? []). Your array reaches React as one serialised string, so the reference identity that useEffect compares never enters into it.

What that means in practice — the middle column is actual JSON.stringify output for the value wrapped in an array:

Value in depsSerialises toEffect
Primitives: string, number, boolean, nullthemselvesCompared correctly. Prefer these.
A functionnullNever re-registers — every function serialises identically.
undefined, a SymbolnullNever re-registers.
new Map([["a", 1]]), new Set([1, 2, 3]){}Never re-registers — an empty and a populated Map compare equal.
Class instance with own enumerable fields (this.n = n){"n":1}Compared correctly.
Class instance whose state is in #private fields{}Never re-registers.
Class instance whose state is behind a prototype getter{} (only own fields appear)Never re-registers on the getter's value.
{ x: 1, y: 2 } vs { y: 2, x: 1 }{"x":1,"y":2} vs {"y":2,"x":1}Re-registers even though the objects are equivalent — key order counts.
A circular object, a BigIntthrows TypeErrorCrashes the component — see below.
<Callout type="warn"> A circular value in `deps` is not a silent bug. `JSON.stringify` throws `TypeError: Converting circular structure to JSON` while the component is rendering, which takes the component down. Never put a value that may reference itself — a graph node, a parent-linked tree, a store instance — into `deps`. A `BigInt` throws the same way (`Do not know how to serialize a BigInt`). </Callout>

Passing values that do not serialise

Two patterns work.

Derive a primitive that changes exactly when the thing you care about changes, and put that in deps:

tsx
// Not `[items]`, `[cache]`, `[onSubmit]` — those are invisible or unstable.
useFrontendTool({ ...tool }, [items.length, cache.size, selectedId, revision]);

Or keep the value out of deps and read it through a latest-value ref. The registration stays stable while the closure always sees the current value. This is the only option for a changing callback, which deps cannot detect at all:

tsx
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, and to any Map, Set or class instance you would otherwise have been tempted to list in deps.