Skip to content

React Adapter

The @presage-kit/react package provides React components and hooks for building adaptive interfaces.

Terminal window
pnpm add @presage-kit/core @presage-kit/react

Provides the AdaptiveClient to the entire component tree via React context.

import { AdaptiveProvider } from '@presage-kit/react'
import { createAdaptiveClient, createLocalStorageDriver } from '@presage-kit/core'
const client = createAdaptiveClient({
rules: [/* ... */],
persistence: { driver: createLocalStorageDriver('my-app') },
})
function App() {
return (
<AdaptiveProvider client={client}>
<YourApp />
</AdaptiveProvider>
)
}

Props:

PropTypeDescription
clientAdaptiveClientThe client instance from createAdaptiveClient()
childrenReactNodeYour application tree

Declares an adaptation point and renders the matching variant.

import { Adaptive, Variant } from '@presage-kit/react'
function Dashboard() {
return (
<Adaptive id="dashboard-layout" defaultVariant="standard">
<Variant id="compact">
<CompactDashboard />
</Variant>
<Variant id="standard">
<StandardDashboard />
</Variant>
<Variant id="advanced">
<AdvancedDashboard />
</Variant>
</Adaptive>
)
}

<Adaptive> props:

PropTypeDescription
idstringUnique adaptation point identifier
defaultVariantstringVariant to show when no rule matches
childrenReactNodeMust contain <Variant> components

<Variant> props:

PropTypeDescription
idstringVariant identifier (referenced in rule actions)
childrenReactNodeContent to render when this variant is selected

The <Adaptive> component internally uses useAdaptive() and automatically tracks impressions when the selected variant changes.

Renders a list of navigation items that can be reordered by adaptation rules.

import { AdaptiveNav, type NavItem } from '@presage-kit/react'
interface MenuItem extends NavItem {
label: string
href: string
icon: string
}
const menuItems: MenuItem[] = [
{ id: 'home', label: 'Home', href: '/', icon: 'home' },
{ id: 'analytics', label: 'Analytics', href: '/analytics', icon: 'chart' },
{ id: 'settings', label: 'Settings', href: '/settings', icon: 'gear' },
]
function Sidebar() {
return (
<AdaptiveNav
id="sidebar-nav"
items={menuItems}
renderItem={(item, index) => (
<a key={item.id} href={item.href}>
{item.icon} {item.label}
</a>
)}
/>
)
}

With a reorder rule, the navigation order changes based on user context:

{
id: 'power-user-nav-order',
adaptationId: 'sidebar-nav',
priority: 10,
conditions: {
all: [{ field: 'maturity', operator: 'eq', value: 'power' }],
},
action: { type: 'reorder', order: ['analytics', 'home', 'settings'] },
}

With a hide rule, the entire navigation is hidden:

{
id: 'hide-nav-for-new-users',
adaptationId: 'sidebar-nav',
priority: 20,
conditions: {
all: [{ field: 'maturity', operator: 'eq', value: 'new' }],
},
action: { type: 'hide' },
}

Props:

PropTypeDescription
idstringAdaptation point identifier
itemsT[] (extends NavItem)Array of items, each with at least an id field
renderItem(item: T, index: number) => ReactNodeRender function for each item

NavItem interface:

interface NavItem {
id: string
[key: string]: unknown
}

The primary hook for resolving adaptation points. Returns the selected variant, full resolution details, context, and a scoped track function.

import { useAdaptive } from '@presage-kit/react'
function FeatureDiscovery() {
const { selectedVariant, resolution, context, track } = useAdaptive('feature-discovery', {
variants: ['tooltip', 'banner', 'modal'] as const,
defaultVariant: 'tooltip',
})
// Track a conversion event scoped to this adaptation point
function handleDismiss() {
track('feature_discovery_dismissed')
}
switch (selectedVariant) {
case 'tooltip':
return <Tooltip onDismiss={handleDismiss} />
case 'banner':
return <Banner onDismiss={handleDismiss} />
case 'modal':
return <Modal onDismiss={handleDismiss} />
}
}

Parameters:

ParamTypeDescription
adaptationIdstringUnique identifier for the adaptation point
config.variantsreadonly string[]Available variant IDs
config.defaultVariantstringFallback variant
config.strategyStrategyOptional. Defaults to { type: 'rules' }

Return type:

FieldTypeDescription
selectedVariantVThe selected variant (type-narrowed to your variant union)
resolutionResolvedAdaptationFull resolution details (strategy, reason, timestamp)
contextUserContextCurrent user context
track(event: string, properties?: Record<string, unknown>) => voidTrack function scoped to this adaptation point

TypeScript generics: The hook infers the variant type from variants:

// selectedVariant is typed as 'tooltip' | 'banner' | 'modal'
const { selectedVariant } = useAdaptive('feature-discovery', {
variants: ['tooltip', 'banner', 'modal'] as const,
defaultVariant: 'tooltip',
})

Automatic impression tracking: When the selected variant changes, useAdaptive() automatically tracks an presage:impression event with the adaptation ID, variant, and strategy.

A lighter hook that returns only the selected variant string. Use this when you don’t need the full resolution details.

import { useVariant } from '@presage-kit/react'
function CTAButton() {
const variant = useVariant('cta-style', {
variants: ['primary', 'secondary', 'subtle'] as const,
defaultVariant: 'primary',
})
return <button className={`cta cta--${variant}`}>Get Started</button>
}

Parameters: Same as useAdaptive().

Return type: V — The selected variant string.

Returns the AdaptiveClient instance from context. Useful for imperative operations like tracking and identifying.

import { useAdaptiveClient } from '@presage-kit/react'
function TrackingButton() {
const client = useAdaptiveClient()
function handleClick() {
client.track('feature_used', { featureId: 'export' })
}
return <button onClick={handleClick}>Export</button>
}

Return type: AdaptiveClient

Throws an error if called outside of <AdaptiveProvider>.

This example shows an adaptive sidebar and onboarding flow for a SaaS dashboard.

src/lib/adaptive-client.ts
import { createAdaptiveClient, createLocalStorageDriver } from '@presage-kit/core'
export const client = createAdaptiveClient({
rules: [
// Sidebar: prioritize analytics for power users
{
id: 'power-user-sidebar',
adaptationId: 'sidebar',
priority: 10,
conditions: {
all: [{ field: 'maturity', operator: 'eq', value: 'power' }],
},
action: { type: 'reorder', order: ['analytics', 'dashboards', 'settings', 'home'] },
},
// Onboarding: guided tour for new users
{
id: 'new-user-tour',
adaptationId: 'onboarding',
priority: 10,
conditions: {
all: [{ field: 'maturity', operator: 'eq', value: 'new' }],
},
action: { type: 'show', variantId: 'guided-tour' },
},
// Onboarding: hide for experienced users
{
id: 'experienced-no-onboarding',
adaptationId: 'onboarding',
priority: 5,
conditions: {
any: [
{ field: 'maturity', operator: 'eq', value: 'active' },
{ field: 'maturity', operator: 'eq', value: 'power' },
],
},
action: { type: 'hide' },
},
],
persistence: {
driver: createLocalStorageDriver('saas-demo'),
},
})
src/App.tsx
import { AdaptiveProvider } from '@presage-kit/react'
import { client } from './lib/adaptive-client'
import { Sidebar } from './components/Sidebar'
import { Onboarding } from './components/Onboarding'
import { Dashboard } from './components/Dashboard'
export function App() {
return (
<AdaptiveProvider client={client}>
<div style={{ display: 'flex' }}>
<Sidebar />
<main>
<Onboarding />
<Dashboard />
</main>
</div>
</AdaptiveProvider>
)
}
src/components/Sidebar.tsx
import { AdaptiveNav, type NavItem } from '@presage-kit/react'
interface SidebarItem extends NavItem {
label: string
href: string
}
const items: SidebarItem[] = [
{ id: 'home', label: 'Home', href: '/' },
{ id: 'dashboards', label: 'Dashboards', href: '/dashboards' },
{ id: 'analytics', label: 'Analytics', href: '/analytics' },
{ id: 'settings', label: 'Settings', href: '/settings' },
]
export function Sidebar() {
return (
<nav>
<AdaptiveNav
id="sidebar"
items={items}
renderItem={(item) => (
<a key={item.id} href={item.href}>
{item.label}
</a>
)}
/>
</nav>
)
}
src/components/Onboarding.tsx
import { Adaptive, Variant } from '@presage-kit/react'
export function Onboarding() {
return (
<Adaptive id="onboarding" defaultVariant="standard">
<Variant id="guided-tour">
<div>
<h2>Welcome! Let us show you around.</h2>
<p>Follow the steps to get set up.</p>
</div>
</Variant>
<Variant id="standard">
<div>
<h2>What is new</h2>
<p>Check out the latest features.</p>
</div>
</Variant>
</Adaptive>
)
}