React Integration
How to use PACE.js with React.
Overview
PACE.js is framework-agnostic and works great with React. This guide shows you how to:
- ✅ Wrap PACE.js in a React component
- ✅ Synchronize PACE state with React state
- ✅ Handle events in React
- ✅ TypeScript support
Installation
bash
npm install react react-dom
npm install @semanticintent/pace-patternBasic Integration
PACEWrapper Component
tsx
// components/PACEWrapper.tsx
import { PACE } from '@semanticintent/pace-pattern'
import { useEffect, useRef } from 'react'
interface PACEWrapperProps {
products: any[]
onProductSelect?: (product: any) => void
onReady?: () => void
}
export function PACEWrapper({
products,
onProductSelect,
onReady
}: PACEWrapperProps) {
const containerRef = useRef<HTMLDivElement>(null)
const paceRef = useRef<PACE | null>(null)
useEffect(() => {
if (!containerRef.current) return
// Create PACE instance
const pace = new PACE({
container: containerRef.current,
products: { products }
})
// Listen to events
pace.on('ready', () => {
onReady?.()
})
pace.on('product:select', ({ product }) => {
onProductSelect?.(product)
})
// Mount PACE
pace.mount()
paceRef.current = pace
// Cleanup
return () => {
pace.destroy()
paceRef.current = null
}
}, [products, onProductSelect, onReady])
return <div ref={containerRef} />
}Usage
tsx
// App.tsx
import { PACEWrapper } from './components/PACEWrapper'
import { useState } from 'react'
import products from './products.json'
export default function App() {
const [selectedProduct, setSelectedProduct] = useState(null)
return (
<div>
<PACEWrapper
products={products}
onProductSelect={(product) => {
console.log('Selected:', product.name)
setSelectedProduct(product)
}}
onReady={() => {
console.log('PACE ready!')
}}
/>
{selectedProduct && (
<div>
Selected: {selectedProduct.name}
</div>
)}
</div>
)
}Advanced Integration
With State Synchronization
tsx
// hooks/usePACE.ts
import { PACE } from '@semanticintent/pace-pattern'
import { useEffect, useRef, useState } from 'react'
export function usePACE(config) {
const [state, setState] = useState({
activeView: 'product',
selectedProduct: null,
chatHistory: []
})
const paceRef = useRef<PACE | null>(null)
useEffect(() => {
const pace = new PACE(config)
// Sync PACE state with React state
pace.state.subscribe('activeView', (value) => {
setState(prev => ({ ...prev, activeView: value }))
})
pace.state.subscribe('selectedProduct', (value) => {
setState(prev => ({ ...prev, selectedProduct: value }))
})
pace.state.subscribe('chatHistory', (value) => {
setState(prev => ({ ...prev, chatHistory: value }))
})
pace.mount()
paceRef.current = pace
return () => {
pace.destroy()
}
}, [config])
return {
pace: paceRef.current,
state
}
}Usage:
tsx
function App() {
const { pace, state } = usePACE({
container: '#app',
products: './products.json'
})
return (
<div>
<p>Active view: {state.activeView}</p>
<p>Chat messages: {state.chatHistory.length}</p>
{state.selectedProduct && (
<p>Selected: {state.selectedProduct.name}</p>
)}
</div>
)
}With React Router
tsx
// App.tsx
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import { PACEWrapper } from './components/PACEWrapper'
export default function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/products" element={
<PACEWrapper
products={products}
defaultView="product"
/>
} />
<Route path="/chat" element={
<PACEWrapper
products={products}
defaultView="chat"
/>
} />
</Routes>
</BrowserRouter>
)
}With Context
tsx
// contexts/PACEContext.tsx
import { createContext, useContext, useEffect, useRef } from 'react'
import { PACE } from '@semanticintent/pace-pattern'
const PACEContext = createContext<PACE | null>(null)
export function PACEProvider({ children, config }) {
const paceRef = useRef<PACE | null>(null)
useEffect(() => {
const pace = new PACE(config)
pace.mount()
paceRef.current = pace
return () => {
pace.destroy()
}
}, [config])
return (
<PACEContext.Provider value={paceRef.current}>
{children}
</PACEContext.Provider>
)
}
export function usePACEContext() {
const context = useContext(PACEContext)
if (!context) {
throw new Error('usePACEContext must be used within PACEProvider')
}
return context
}Usage:
tsx
function App() {
return (
<PACEProvider config={{ ... }}>
<ProductList />
<ChatWidget />
</PACEProvider>
)
}
function ProductList() {
const pace = usePACEContext()
const handleSelect = (productId) => {
pace.navigate('product', { id: productId })
}
return <div>...</div>
}TypeScript Support
Type Definitions
typescript
// types/pace.d.ts
import { PACE as BasePACE } from '@semanticintent/pace-pattern'
export interface Product {
id: string
name: string
tagline: string
category: string
description: string
action_label?: string
action_url?: string
}
export interface PACEConfig {
container: string | HTMLElement
products: Product[] | { products: Product[] } | string
aiAdapter?: any
greeting?: string
defaultView?: 'product' | 'about' | 'chat' | 'summary'
theme?: {
primary?: string
accent?: string
font?: string
}
}
export interface PACEState {
activeView: string
selectedProduct: Product | null
chatHistory: any[]
executiveSummaryData: any
}
declare module '@semanticintent/pace-pattern' {
export class PACE extends BasePACE {
constructor(config: PACEConfig)
state: {
get(key: string): any
set(key: string, value: any): void
subscribe(key: string, callback: Function): Function
}
}
}Typed Component
tsx
import { PACE, PACEConfig, Product } from '@/types/pace'
interface PACEWrapperProps {
config: PACEConfig
onProductSelect?: (product: Product) => void
}
export function PACEWrapper({ config, onProductSelect }: PACEWrapperProps) {
// ... implementation
}Performance Optimization
Memoization
tsx
import { memo } from 'react'
export const PACEWrapper = memo(function PACEWrapper({ products, ...props }) {
// ... implementation
}, (prevProps, nextProps) => {
// Custom comparison
return prevProps.products === nextProps.products
})Lazy Loading
tsx
import { lazy, Suspense } from 'react'
const PACEWrapper = lazy(() => import('./components/PACEWrapper'))
function App() {
return (
<Suspense fallback={<div>Loading PACE...</div>}>
<PACEWrapper products={products} />
</Suspense>
)
}Server-Side Rendering (Next.js)
tsx
// components/PACEWrapper.tsx
'use client' // Mark as client component
import { PACE } from '@semanticintent/pace-pattern'
import { useEffect, useRef } from 'react'
export function PACEWrapper({ products }) {
const containerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
// Only run on client
if (typeof window === 'undefined') return
const pace = new PACE({
container: containerRef.current!,
products: { products }
})
pace.mount()
return () => {
pace.destroy()
}
}, [products])
return <div ref={containerRef} />
}Usage in Next.js:
tsx
// app/page.tsx
import { PACEWrapper } from '@/components/PACEWrapper'
import products from '@/data/products.json'
export default function Home() {
return (
<main>
<PACEWrapper products={products} />
</main>
)
}Complete Example
tsx
// App.tsx
import { useState } from 'react'
import { PACE, ClaudeAdapter } from '@semanticintent/pace-pattern'
import { PACEWrapper } from './components/PACEWrapper'
import products from './products.json'
export default function App() {
const [selectedProduct, setSelectedProduct] = useState(null)
const [chatCount, setChatCount] = useState(0)
const paceConfig = {
container: '#pace-container',
products: { products },
aiAdapter: new ClaudeAdapter({
apiKey: import.meta.env.VITE_CLAUDE_API_KEY,
model: 'claude-3-sonnet-20240229'
}),
greeting: 'Welcome to our store! What can I help you find?'
}
return (
<div className="app">
<header>
<h1>My PACE Store</h1>
{selectedProduct && (
<p>Viewing: {selectedProduct.name}</p>
)}
<p>Chat messages: {chatCount}</p>
</header>
<PACEWrapper
config={paceConfig}
onProductSelect={setSelectedProduct}
onChatMessage={() => setChatCount(c => c + 1)}
/>
</div>
)
}Best Practices
1. Cleanup on Unmount
Always destroy PACE instance:
tsx
useEffect(() => {
const pace = new PACE(config)
pace.mount()
return () => {
pace.destroy() // ✅ Cleanup
}
}, [])2. Avoid Re-creating Unnecessarily
tsx
// ✅ Good - config in dependency array
useEffect(() => {
const pace = new PACE(config)
// ...
}, [config])
// ❌ Bad - missing dependencies
useEffect(() => {
const pace = new PACE(config)
// ...
}, [])3. Use Refs for PACE Instance
tsx
const paceRef = useRef<PACE | null>(null)
// Access PACE methods
const handleAction = () => {
paceRef.current?.navigate('chat')
}Resources
- Example Repo: pace.js/examples/react
- TypeScript Definitions: Included in
@semanticintent/pace-pattern - React Docs: react.dev
PACE.js + React = Perfect match! ⚛️