Skip to main content

React Integration

The @plucky-ai/react package provides React hooks for integrating the Plucky chat widget with proper lifecycle management. This is a companion to the Chat SDK.

Installation

npm install @plucky-ai/react @plucky-ai/chat-sdk
# or
yarn add @plucky-ai/react @plucky-ai/chat-sdk
# or
pnpm add @plucky-ai/react @plucky-ai/chat-sdk

Requirements

  • React 18 or higher
  • @plucky-ai/chat-sdk as a peer dependency

useChat

The useChat hook initializes the Plucky chat widget in your React application.

Basic Usage

import { useChat } from '@plucky-ai/react'

function App() {
useChat({
appId: 'your-app-id',
baseUrl: 'https://widget.plucky.ai',
user: {
id: 'user-123',
name: 'John Doe',
email: '[email protected]',
},
})

return <div>Your app content</div>
}

With Dynamic User Data

import { useChat } from '@plucky-ai/react'
import { useAuth } from './your-auth-provider'

function App() {
const { user } = useAuth()

useChat({
appId: 'your-app-id',
baseUrl: 'https://widget.plucky.ai',
user: user
? {
id: user.id,
name: user.name,
email: user.email,
}
: undefined,
})

return <div>Your app content</div>
}

Parameters

The hook accepts the same options as the Plucky() function. See the SDK Reference for the complete list of configuration options.


useTools

The useTools hook registers tools with the AI assistant while properly managing React's component lifecycle. It automatically adds tools when the component mounts and removes them when it unmounts.

Why Use useTools?

When working with React, you should prefer useTools over calling addTools() directly because:

  1. Automatic cleanup - Tools are removed when the component unmounts
  2. Proper callback references - The context pattern ensures callbacks access current state
  3. React lifecycle integration - Tools are re-registered when dependencies change

Basic Usage

import { useTools } from '@plucky-ai/react'

function OrderDashboard() {
useTools({
tools: [
{
name: 'get_recent_orders',
description: 'Get the most recent orders for the current user',
defaultLoadingText: 'Fetching orders...',
defaultSuccessText: 'Orders retrieved.',
cb: async () => {
const orders = await fetchRecentOrders()
return JSON.stringify(orders, null, 2)
},
},
],
})

return <div>Order Dashboard</div>
}

Using Context for React State

The context parameter solves a common React problem: accessing current state values inside callbacks. Without it, callbacks would capture stale values due to JavaScript closures.

import { useTools } from '@plucky-ai/react'
import { useState } from 'react'

function TemperatureWidget() {
const [temperature, setTemperature] = useState(72)
const [unit, setUnit] = useState<'celsius' | 'fahrenheit'>('fahrenheit')

useTools({
// Pass React state/props that tools need access to
context: {
temperature,
unit,
},
tools: [
{
name: 'get_current_temperature',
description: 'Get the current temperature reading',
cb: async (input, ctx) => {
// ctx contains the latest values of temperature and unit
return `Current temperature: ${ctx.temperature}°${ctx.unit === 'celsius' ? 'C' : 'F'}`
},
},
{
name: 'set_temperature',
description: 'Set the target temperature',
inputSchema: z.object({
value: z.number().describe('The temperature value to set'),
}),
cb: async (input, ctx) => {
// Note: You can't call setTemperature directly here
// Instead, return data and let the parent handle state updates
return {
content: `Temperature will be set to ${input.value}°${ctx.unit === 'celsius' ? 'C' : 'F'}`,
successText: `Set to ${input.value}°`,
}
},
},
],
})

return (
<div>
<p>
Temperature: {temperature}°{unit === 'celsius' ? 'C' : 'F'}
</p>
<button onClick={() => setTemperature((t) => t + 1)}>+</button>
<button onClick={() => setTemperature((t) => t - 1)}>-</button>
</div>
)
}

TypeScript Generics

For type-safe context access, use the generic parameter:

import { useTools } from '@plucky-ai/react'

interface MyContext {
userId: string
permissions: string[]
apiKey: string
}

function AdminPanel({ userId, permissions, apiKey }: MyContext) {
useTools<MyContext>({
context: {
userId,
permissions,
apiKey,
},
tools: [
{
name: 'admin_action',
description: 'Perform an admin action',
cb: async (input, ctx) => {
// ctx is typed as MyContext
if (!ctx.permissions.includes('admin')) {
return 'Permission denied'
}
// ...
return 'Action completed'
},
},
],
})

return <div>Admin Panel</div>
}

Tools with Input Schemas

import { useTools } from '@plucky-ai/react'
import { z } from 'zod'

const SearchSchema = z.object({
query: z.string().describe('The search query'),
limit: z.number().default(10).describe('Maximum results to return'),
category: z
.enum(['all', 'products', 'articles', 'users'])
.default('all')
.describe('Category to search in'),
})

function SearchableContent() {
useTools({
tools: [
{
name: 'search',
description: 'Search for content across the application',
inputSchema: SearchSchema,
defaultLoadingText: 'Searching...',
cb: async (input) => {
const { query, limit, category } = SearchSchema.parse(input)
const results = await performSearch(query, { limit, category })

return {
content: JSON.stringify(results, null, 2),
successText: `Found ${results.length} results`,
}
},
},
],
})

return <div>Searchable Content</div>
}

Common Patterns

Initializing in a Root Provider

Initialize the chat once at the root of your application:

// providers/PluckyProvider.tsx
import { useChat } from '@plucky-ai/react'
import { useAuth } from './AuthProvider'

export function PluckyProvider({ children }: { children: React.ReactNode }) {
const { user, isAuthenticated } = useAuth()

useChat({
appId: process.env.NEXT_PUBLIC_PLUCKY_APP_ID!,
baseUrl: process.env.NEXT_PUBLIC_PLUCKY_BASE_URL!,
user: isAuthenticated
? {
id: user.id,
name: user.name,
email: user.email,
metadata: {
role: user.role,
},
}
: undefined,
})

return <>{children}</>
}

// app/layout.tsx or _app.tsx
import { PluckyProvider } from './providers/PluckyProvider'

export default function RootLayout({ children }) {
return (
<AuthProvider>
<PluckyProvider>{children}</PluckyProvider>
</AuthProvider>
)
}

Conditional Tool Registration

Register tools only on specific pages or based on user permissions:

import { useTools } from '@plucky-ai/react'
import { useAuth } from './AuthProvider'

function AdminPage() {
const { user } = useAuth()
const isAdmin = user?.role === 'admin'

useTools({
context: { user },
tools: isAdmin
? [
{
name: 'delete_user',
description: 'Delete a user account (admin only)',
inputSchema: z.object({
userId: z.string(),
}),
cb: async (input, ctx) => {
await deleteUser(input.userId)
return `User ${input.userId} deleted`
},
},
]
: [],
})

return <div>Admin Page</div>
}

Route-Specific Tools

Register different tools based on the current route:

// pages/orders/[orderId].tsx
import { useTools } from '@plucky-ai/react'
import { useRouter } from 'next/router'

function OrderDetailPage() {
const router = useRouter()
const { orderId } = router.query

useTools({
context: { orderId: orderId as string },
tools: [
{
name: 'get_order_details',
description: 'Get details for the current order',
cb: async (_, ctx) => {
const order = await fetchOrder(ctx.orderId)
return JSON.stringify(order, null, 2)
},
},
{
name: 'cancel_order',
description: 'Cancel the current order',
cb: async (_, ctx) => {
await cancelOrder(ctx.orderId)
return {
content: `Order ${ctx.orderId} has been cancelled`,
successText: 'Order cancelled',
}
},
},
],
})

return <div>Order {orderId}</div>
}

Error Handling

Always handle errors gracefully in your tool callbacks:

useTools({
tools: [
{
name: 'risky_operation',
description: 'An operation that might fail',
cb: async (input) => {
try {
const result = await performRiskyOperation(input)
return `Success: ${result}`
} catch (error) {
// Return a helpful error message instead of throwing
if (error instanceof NotFoundError) {
return `Could not find the requested resource. Please check the ID and try again.`
}
if (error instanceof PermissionError) {
return `You don't have permission to perform this action.`
}
// Generic fallback
return `An error occurred: ${error.message}`
}
},
},
],
})

Next Steps