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​