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:
The name of the tool to render. Use "*" for a wildcard renderer that handles all unmatched tools.
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.
Scope this renderer to a specific agent. If specified, only tool calls from that agent will use this renderer.
Dependencies
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
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:
Exact name + agentId match - Highest priority
Exact name match (no agentId) - Medium priority
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.
Feature useRenderTool useFrontendTool Handler No handler Optional handler Registration Renderer only Tool + renderer Use Case Backend tools, custom UI Frontend tools with logic Tool Execution Backend/external Frontend 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 ;
}