Skip to main content
The useRenderTool hook allows you to customize how tool calls are displayed in the UI without implementing the tool’s execution logic. This is useful for rendering backend tools or providing wildcard renderers for multiple tools.

Basic Usage

import { useRenderTool } from "@copilotkit/react-core";
import { z } from "zod";

function SearchRenderer() {
  useRenderTool(
    {
      name: "searchDocs",
      parameters: z.object({
        query: z.string()
      }),
      render: ({ status, parameters, result }) => {
        if (status === "executing") {
          return <div>Searching for: {parameters.query}...</div>;
        }
        if (status === "complete") {
          return <div>Found results for: {parameters.query}</div>;
        }
        return <div>Preparing search...</div>;
      }
    },
    []
  );
  
  return null;
}

Parameters

The hook accepts a configuration object:
name
string
required
The name of the tool to render. Use "*" for a wildcard renderer that handles all unmatched tools.
parameters
z.ZodType
Zod schema defining the tool’s parameters. Provides type inference for the parameters prop in your render function.Not required for wildcard renderers (name: "*").
render
(props: RenderToolProps) => React.ReactElement
required
Function that returns a React element to render the tool call.
agentId
string
Scope this renderer to a specific agent. If specified, only tool calls from that agent will use this renderer.

Dependencies

deps
ReadonlyArray<unknown>
Optional dependency array (like useEffect). When dependencies change, the renderer is re-registered.

Render Props

Your render function receives props that vary based on the tool call status:

inProgress Status

{
  name: string;
  parameters: Partial<T>;
  status: "inProgress";
  result: undefined;
}

executing Status

{
  name: string;
  parameters: T;
  status: "executing";
  result: undefined;
}

complete Status

{
  name: string;
  parameters: T;
  status: "complete";
  result: string;
}

Examples

Rendering Backend Tools

import { useRenderTool } from "@copilotkit/react-core";
import { z } from "zod";

function BackendToolRenderers() {
  // Render a database query tool
  useRenderTool(
    {
      name: "queryDatabase",
      parameters: z.object({
        query: z.string(),
        table: z.string()
      }),
      render: ({ status, parameters, result }) => {
        if (status === "inProgress") {
          return <div className="tool-progress">Preparing query...</div>;
        }
        
        if (status === "executing") {
          return (
            <div className="tool-executing">
              <span className="spinner"></span>
              Querying {parameters.table}...
            </div>
          );
        }
        
        if (status === "complete") {
          const data = JSON.parse(result);
          return (
            <div className="tool-complete">
              <h4>Query Results</h4>
              <pre>{JSON.stringify(data, null, 2)}</pre>
            </div>
          );
        }
      }
    },
    []
  );
  
  return null;
}

Wildcard Renderer

import { useRenderTool } from "@copilotkit/react-core";

function DefaultToolRenderer() {
  useRenderTool(
    {
      name: "*", // Matches all tools without a specific renderer
      render: ({ name, status, parameters, result }) => {
        return (
          <div className="default-tool-render">
            <div className="tool-header">
              <strong>{name}</strong>
              <span className={`status ${status}`}>{status}</span>
            </div>
            
            {status !== "inProgress" && (
              <div className="tool-args">
                <pre>{JSON.stringify(parameters, null, 2)}</pre>
              </div>
            )}
            
            {status === "complete" && result && (
              <div className="tool-result">
                <strong>Result:</strong>
                <pre>{result}</pre>
              </div>
            )}
          </div>
        );
      }
    },
    []
  );
  
  return null;
}

Progressive Rendering

import { useRenderTool } from "@copilotkit/react-core";
import { z } from "zod";

function ProgressiveRenderer() {
  useRenderTool(
    {
      name: "analyzeDocument",
      parameters: z.object({
        documentId: z.string(),
        sections: z.array(z.string())
      }),
      render: ({ status, parameters, result }) => {
        // Show different UI at each stage
        if (status === "inProgress") {
          return (
            <div className="analysis-starting">
              <div className="skeleton-loader" />
              <p>Loading document...</p>
            </div>
          );
        }
        
        if (status === "executing") {
          return (
            <div className="analysis-running">
              <h4>Analyzing Document</h4>
              <ul>
                {parameters.sections?.map(section => (
                  <li key={section}>
                    <span className="spinner"></span> {section}
                  </li>
                ))}
              </ul>
            </div>
          );
        }
        
        if (status === "complete") {
          const analysis = JSON.parse(result);
          return (
            <div className="analysis-complete">
              <h4>✓ Analysis Complete</h4>
              <div className="summary">{analysis.summary}</div>
              <div className="metrics">
                {Object.entries(analysis.metrics).map(([key, value]) => (
                  <div key={key}>
                    <strong>{key}:</strong> {String(value)}
                  </div>
                ))}
              </div>
            </div>
          );
        }
      }
    },
    []
  );
  
  return null;
}

Agent-Specific Renderer

import { useRenderTool } from "@copilotkit/react-core";
import { z } from "zod";

function ResearchAgentRenderers() {
  // Renderer specifically for the research agent
  useRenderTool(
    {
      name: "citePaper",
      parameters: z.object({
        title: z.string(),
        authors: z.array(z.string()),
        year: z.number()
      }),
      agentId: "research-agent",
      render: ({ status, parameters, result }) => {
        if (status === "complete") {
          return (
            <div className="citation">
              <p>
                {parameters.authors.join(", ")} ({parameters.year}).{" "}
                <em>{parameters.title}</em>.
              </p>
            </div>
          );
        }
        return <div>Loading citation...</div>;
      }
    },
    []
  );
  
  return null;
}

Handling Incomplete JSON

import { useRenderTool } from "@copilotkit/react-core";
import { z } from "zod";

function StreamingRenderer() {
  useRenderTool(
    {
      name: "generateReport",
      parameters: z.object({
        title: z.string().optional(),
        sections: z.array(z.string()).optional(),
        format: z.string().optional()
      }),
      render: ({ status, parameters }) => {
        // Handle partial parameters gracefully during streaming
        return (
          <div className="report-generator">
            <h3>{parameters.title || "Untitled Report"}</h3>
            
            {parameters.sections && parameters.sections.length > 0 && (
              <div className="sections">
                <strong>Sections:</strong>
                <ul>
                  {parameters.sections.map((section, i) => (
                    <li key={i}>{section}</li>
                  ))}
                </ul>
              </div>
            )}
            
            {parameters.format && (
              <div className="format">
                Format: {parameters.format}
              </div>
            )}
            
            {status === "executing" && (
              <div className="spinner">Generating...</div>
            )}
          </div>
        );
      }
    },
    []
  );
  
  return null;
}

Custom Styling by Status

import { useRenderTool } from "@copilotkit/react-core";
import { z } from "zod";

function StyledRenderer() {
  useRenderTool(
    {
      name: "processPayment",
      parameters: z.object({
        amount: z.number(),
        currency: z.string()
      }),
      render: ({ status, parameters, result }) => {
        const statusColors = {
          inProgress: "#999",
          executing: "#2196F3",
          complete: "#4CAF50"
        };
        
        const statusIcons = {
          inProgress: "⏳",
          executing: "💳",
          complete: "✓"
        };
        
        return (
          <div 
            style={{
              border: `2px solid ${statusColors[status]}`,
              borderRadius: "8px",
              padding: "16px",
              marginBottom: "8px"
            }}
          >
            <div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
              <span style={{ fontSize: "24px" }}>
                {statusIcons[status]}
              </span>
              <div>
                <strong>Payment Processing</strong>
                <div>
                  {parameters.amount} {parameters.currency}
                </div>
              </div>
            </div>
            
            {status === "complete" && result && (
              <div style={{ marginTop: "12px", color: "#4CAF50" }}>
                ✓ Payment successful
              </div>
            )}
          </div>
        );
      }
    },
    []
  );
  
  return null;
}

Renderer Priority

When multiple renderers match a tool call, CopilotKit uses this priority:
  1. Exact name + agentId match - Highest priority
  2. Exact name match (no agentId) - Medium priority
  3. Wildcard renderer ("*") - Lowest priority (fallback)

Renderer Persistence

Renderers are not removed on component unmount. This ensures that historical tool calls in the chat history continue to render correctly even after the component that registered the renderer is unmounted.

Dynamic Renderer Updates

import { useRenderTool } from "@copilotkit/react-core";
import { z } from "zod";
import { useState } from "react";

function DynamicRenderer() {
  const [theme, setTheme] = useState<"light" | "dark">("light");
  
  useRenderTool(
    {
      name: "myTool",
      parameters: z.object({ data: z.string() }),
      render: ({ parameters }) => (
        <div className={`tool-render theme-${theme}`}>
          {parameters.data}
        </div>
      )
    },
    [theme] // Re-register when theme changes
  );
  
  return (
    <button onClick={() => setTheme(t => t === "light" ? "dark" : "light")}>
      Toggle Theme
    </button>
  );
}

Best Practices

Handle All Statuses

Always provide UI for all three statuses (inProgress, executing, complete) to ensure a smooth user experience.

Use Wildcards Wisely

Wildcard renderers are great for consistent styling, but be careful not to override specific tool renderers unintentionally.

Optimize Re-renders

Only include necessary dependencies in the deps array. Unnecessary re-registrations can impact performance.

Handle Partial Parameters

During streaming (inProgress status), parameters may be incomplete. Always check for optional fields and provide defaults.

Comparison with useFrontendTool

FeatureuseRenderTooluseFrontendTool
HandlerNo handlerOptional handler
RegistrationRenderer onlyTool + renderer
Use CaseBackend tools, custom UIFrontend tools with logic
Tool ExecutionBackend/externalFrontend JavaScript

TypeScript

interface RenderToolConfig<S extends z.ZodTypeAny> {
  name: string | "*";
  parameters?: S;
  render: (props: RenderToolProps<S>) => React.ReactElement;
  agentId?: string;
}

type RenderToolProps<S extends z.ZodTypeAny> =
  | RenderToolInProgressProps<S>
  | RenderToolExecutingProps<S>
  | RenderToolCompleteProps<S>;

interface RenderToolInProgressProps<S> {
  name: string;
  parameters: Partial<z.infer<S>>;
  status: "inProgress";
  result: undefined;
}

interface RenderToolExecutingProps<S> {
  name: string;
  parameters: z.infer<S>;
  status: "executing";
  result: undefined;
}

interface RenderToolCompleteProps<S> {
  name: string;
  parameters: z.infer<S>;
  status: "complete";
  result: string;
}