Skip to content

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-pattern

Basic 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


PACE.js + React = Perfect match! ⚛️