React SDK

The Uniform SDK for React provides two complementary packages for building personalized, composable experiences in any React application:

  • @uniformdev/canvas-react -- Renders Uniform Canvas compositions as React component trees with support for slots, parameters, rich text, and visual editing.
  • @uniformdev/context-react -- Provides React bindings for Uniform Context, enabling personalization, A/B testing, visitor scoring, and behavior tracking.

These packages work with any React setup -- Vite, Remix, Gatsby, a custom SSR server, or any other React-based architecture. For Next.js App Router projects, use the dedicated @uniformdev/next-app-router package instead.


  • React 18+
  • Node.js 18+
  • A Uniform project with compositions and component types configured

Two official starter projects are available on GitHub to help you get up and running quickly:

StarterDescriptionArchitecture
react-starter-craContext-only personalization with React Router. No Canvas compositions -- ideal for adding personalization to an existing React SPA.Create React App, client-side only
react-vite-ssrFull Canvas + Context integration with server-side rendering, including visual editor preview setup.Vite + Express SSR with hydration

Both starters include sample content that you can push to your own Uniform project with npm run uniform:push.

npm install @uniformdev/canvas-react@latest @uniformdev/context-react@latest @uniformdev/canvas@latest @uniformdev/context@latest
PackagePurpose
@uniformdev/canvas-reactReact components for rendering Canvas compositions
@uniformdev/context-reactReact provider and hooks for personalization and testing
@uniformdev/canvasCore Canvas client, types, and utilities
@uniformdev/contextCore Context engine for scoring and personalization

Both React packages require react and react-dom as peer dependencies. If you use client-side visibility rules via the useClientConditionsComposition hook, you also need immer >= 10:

npm install immer

note

immer is only required if you use useClientConditionsComposition. Most projects do not need it.


Canvas is Uniform's visual composition system. The @uniformdev/canvas-react package provides React components to render Canvas compositions -- turning the JSON composition tree from the Uniform API into a rendered React component tree.

A Canvas composition is a tree structure where:

  • The root is a composition instance (e.g., a "Page" component)
  • Each component can have slots -- named locations where child components are placed
  • Each component has parameters -- typed values (text, rich text, images, links, etc.) configured in the Uniform dashboard
  • Personalization and A/B test containers are special system components that wrap variants

The React SDK walks this tree and renders each node using a component resolver -- a function you write that maps Uniform component types to your React components.

Before rendering, you need to fetch the composition data from the Uniform API. Use RouteClient from @uniformdev/canvas:

// uniform/api.ts import { RouteClient } from "@uniformdev/canvas"; const routeClient = new RouteClient({ projectId: process.env.UNIFORM_PROJECT_ID, apiKey: process.env.UNIFORM_API_KEY, }); export async function getComposition(path: string) { const response = await routeClient.getRoute({ path }); if (response.type === "composition") { return response.compositionApiResponse.composition; } return null; }

note

You can find your project ID and API key under Settings > API Keys in your Uniform project. See the API keys guide for details.

The UniformComposition component is the entry point for rendering a composition tree. It takes the composition data and a component resolver, and renders the full component tree:

import { UniformComposition } from "@uniformdev/canvas-react"; import { UniformContext } from "@uniformdev/context-react"; import { Context } from "@uniformdev/context"; import { resolveRenderer } from "./resolveRenderer"; import { manifest } from "./uniform/manifest"; // Create the Context instance (typically done once at app level) const context = new Context({ manifest, defaultConsent: true, }); function App({ composition }) { return ( <UniformContext context={context}> <UniformComposition data={composition} resolveRenderer={resolveRenderer} behaviorTracking="onLoad" /> </UniformContext> ); }
PropTypeRequiredDescription
dataRootComponentInstanceYesThe composition data from the Uniform API
resolveRendererRenderComponentResolverYesFunction that maps component types to React components
behaviorTracking'onLoad' | 'onView'NoWhen to track enrichment tags. 'onView' uses IntersectionObserver (default), 'onLoad' tracks immediately on render
contextualEditingEnhancer(message) => RootComponentInstanceNoEnhancer for contextual editing updates

The component resolver is a function that receives a ComponentInstance and returns the React component to render. This is how you map Uniform component types to your React components:

// resolveRenderer.ts import type { ComponentInstance } from "@uniformdev/canvas"; import { DefaultNotImplementedComponent } from "@uniformdev/canvas-react"; import Page from "./components/Page"; import Hero from "./components/Hero"; import Card from "./components/Card"; export function resolveRenderer(component: ComponentInstance) { switch (component.type) { case "page": return Page; case "hero": return Hero; case "card": return Card; default: return DefaultNotImplementedComponent; } }

tip

Always return a fallback component (such as DefaultNotImplementedComponent) for unknown types. This prevents render errors and shows a helpful message in development that includes the missing component type name and a code snippet to help you register it.

Instead of a switch statement, you can use the component store pattern to register components declaratively:

// components/index.ts import { registerUniformComponent } from "@uniformdev/canvas-react"; import Hero from "./Hero"; import Card from "./Card"; registerUniformComponent({ type: "hero", component: Hero }); registerUniformComponent({ type: "card", component: Card });

Then use componentStoreResolver as your resolver:

import { componentStoreResolver } from "@uniformdev/canvas-react"; import "./components"; // triggers registration <UniformComposition data={composition} resolveRenderer={componentStoreResolver} />

The store supports variant-specific registration:

registerUniformComponent({ type: "hero", variantId: "dark", component: HeroDark, });

Resolution priority: exact type + variant match, then type-only match, then the default fallback.

Every component rendered by Canvas receives ComponentProps. This type gives you access to the raw component data:

import type { ComponentProps } from "@uniformdev/canvas-react"; type HeroProps = ComponentProps<{ title: string; description?: string; buttonText?: string; }>;

The ComponentProps<T> type extends your custom props with:

PropertyTypeDescription
componentComponentInstanceThe raw Canvas component instance with all metadata, slots, and parameters

Parameter values are automatically extracted from component.parameters[id].value and flattened onto the component props by the SDK's internal convertComponentToProps helper. So if your Canvas component has a parameter named title with value "Hello", your component receives { title: "Hello", component: { ... } }.

UniformSlot renders child components placed in a named slot:

import { UniformSlot } from "@uniformdev/canvas-react"; import type { ComponentProps } from "@uniformdev/canvas-react"; const Page = ({ component }: ComponentProps) => { return ( <div> <header> <UniformSlot name="header" /> </header> <main> <UniformSlot name="content" /> </main> <footer> <UniformSlot name="footer" /> </footer> </div> ); }; export default Page;
PropTypeRequiredDescription
namestringYesThe slot name as defined in the Uniform component type
resolveRendererRenderComponentResolverNoOverride the parent's component resolver for this slot's children
wrapperComponentReact.ComponentTypeNoA React component that wraps each item in the slot
emptyPlaceholderReact.ReactNodeNoContent to render when the slot has no children

Use wrapperComponent to wrap each child in the slot with custom markup:

const SlotItemWrapper = ({ children }) => ( <div className="slot-item border-b pb-4 mb-4"> {children} </div> ); <UniformSlot name="content" wrapperComponent={SlotItemWrapper} />

Show fallback content when a slot has no children:

<UniformSlot name="sidebar" emptyPlaceholder={<p className="text-gray-400">No sidebar content</p>} />

UniformText renders text parameters with built-in support for inline editing in the Uniform visual editor:

import { UniformText } from "@uniformdev/canvas-react"; const Hero = () => { return ( <section> <UniformText parameterId="title" as="h1" className="text-4xl font-bold" placeholder="Enter hero title" /> <UniformText parameterId="subtitle" as="p" className="text-lg text-gray-600" placeholder="Enter subtitle" /> </section> ); };
PropTypeRequiredDescription
parameterIdstringYesThe parameter ID as defined in the Uniform component type
asReact.ElementTypeNoHTML element to render (default: 'span')
placeholderstring | ((parameter) => string)NoPlaceholder text shown in the editor when the value is empty
isMultilinebooleanNoEnable line break support (default: false)
render(value: string | undefined) => React.ReactNodeNoCustom render function for transforming the value

Use the render prop to transform the parameter value before display:

<UniformText parameterId="price" render={(value) => value ? `$${parseFloat(value).toFixed(2)}` : null} />

note

UniformText reads the parameter value from the nearest UniformComponent context. It must be used inside a component rendered by UniformComposition or UniformSlot.

UniformRichText renders rich text parameters with full formatting support including headings, lists, links, images, tables, and text formatting:

import { UniformRichText } from "@uniformdev/canvas-react"; const Article = () => { return ( <article> <UniformRichText parameterId="body" className="prose max-w-none" placeholder="Write your article content" /> </article> ); };
PropTypeRequiredDescription
parameterIdstringYesThe parameter ID for the rich text field
asReact.ElementType | nullNoWrapper element (default: 'div'). Set to null for no wrapper
placeholderstring | ((parameter) => string)NoPlaceholder shown in the editor when empty
resolveRichTextRendererRenderRichTextComponentResolverNoCustom renderer for rich text nodes

Override how specific rich text nodes are rendered:

import type { RichTextNode } from "@uniformdev/richtext"; function customRichTextRenderer(node: RichTextNode) { if (node.type === "link") { return ({ node, children }) => ( <a href={node.attrs?.url} className="text-blue-600 hover:underline" target={node.attrs?.target} > {children} </a> ); } // Return null/undefined to use default rendering return null; } <UniformRichText parameterId="body" resolveRichTextRenderer={customRichTextRenderer} />

The SDK includes built-in renderers for all standard rich text node types: paragraphs, headings (h1-h6), lists, list items, links, images/videos/audio assets, tables, line breaks, tabs, and formatted text (bold, italic, underline, strikethrough, code, subscript, superscript).

When you need parameter values directly (for non-visual use like URLs, booleans, or conditional logic), access them from the component prop:

import type { ComponentProps } from "@uniformdev/canvas-react"; type CardProps = ComponentProps<{ title: string; linkUrl?: string; isExternal?: boolean; }>; const Card = ({ title, linkUrl, isExternal, component }: CardProps) => { const Tag = linkUrl ? "a" : "div"; const linkProps = linkUrl ? { href: linkUrl, target: isExternal ? "_blank" : undefined } : {}; return ( <Tag {...linkProps} className="card"> <h3>{title}</h3> </Tag> ); };

For advanced inline editing scenarios, getParameterAttributes returns the data attributes needed to mark an element as editable by the Uniform visual editor:

import { getParameterAttributes } from "@uniformdev/canvas-react"; const CustomText = ({ component }) => { const attrs = getParameterAttributes({ component, id: "title", isMultiline: false, }); return ( <h1 {...attrs}> {component.parameters?.title?.value || "Untitled"} </h1> ); };

This is useful when you cannot use UniformText but still want inline editing support.


Returns the data and configuration from the nearest UniformComposition ancestor:

import { useUniformCurrentComposition } from "@uniformdev/canvas-react"; const Breadcrumb = () => { const { data, isContextualEditing, matchedRoute } = useUniformCurrentComposition(); return ( <nav> <span>Current route: {matchedRoute}</span> {isContextualEditing && <span> (editing)</span>} </nav> ); };

Return value:

PropertyTypeDescription
dataRootComponentInstance | undefinedThe full composition data
isContextualEditingbooleanWhether the composition is being edited in the Uniform visual editor
matchedRoutestring | undefinedThe matched route path
dynamicInputsRecord<string, any> | undefinedDynamic input values from route parameters
resolveRendererRenderComponentResolver | undefinedThe active component resolver
behaviorTracking'onLoad' | 'onView' | undefinedThe behavior tracking mode

Returns the data from the nearest UniformComponent ancestor (the currently rendering component):

import { useUniformCurrentComponent } from "@uniformdev/canvas-react"; const DebugInfo = () => { const { data } = useUniformCurrentComponent(); if (!data) return null; return ( <pre className="text-xs bg-gray-100 p-2 mt-2"> Type: {data.type} {"\n"}Parameters: {JSON.stringify(data.parameters, null, 2)} </pre> ); };

Enables contextual editing -- the two-way communication between your React app and the Uniform visual editor. When a content author edits a composition in the Uniform dashboard, this hook receives live updates and re-renders the composition in real time:

import { UniformComposition, useUniformContextualEditing, } from "@uniformdev/canvas-react"; function PreviewPage({ initialComposition }) { const { composition, isContextualEditing } = useUniformContextualEditing({ initialCompositionValue: initialComposition, enhance: async (composition) => { // Optional: run enhancers on the composition before rendering return composition; }, }); return ( <UniformComposition data={composition} resolveRenderer={resolveRenderer} /> ); }
PropertyTypeDescription
compositionRootComponentInstanceThe current composition data (updates live during editing)
isContextualEditingbooleantrue when the Uniform editor is active

note

Contextual editing is automatically set up when you use UniformComposition with the contextualEditingEnhancer prop. Use useUniformContextualEditing directly only when you need full control over the editing lifecycle.

Provides information about the current contextual editing state, such as which component is selected in the editor:

import { useUniformContextualEditingState } from "@uniformdev/canvas-react"; const ComponentHighlight = ({ children }) => { const { isContextualEditing, selectedComponentReference, previewMode } = useUniformContextualEditingState(); const isSelected = selectedComponentReference?.componentId === component._id; return ( <div className={isSelected ? "ring-2 ring-blue-500" : ""}> {children} {previewMode === "editor" && ( <span className="text-xs text-gray-400">Editor mode</span> )} </div> ); };
PropertyTypeDescription
isContextualEditingbooleanWhether contextual editing is active
selectedComponentReferenceobject | undefinedReference to the currently selected component in the editor
previewMode'editor' | 'preview' | undefinedThe current preview mode

Evaluates client-side visibility rules on a composition and returns a filtered composition with invisible components removed:

import { useClientConditionsComposition } from "@uniformdev/canvas-react"; function PageWithVisibility({ composition }) { // Evaluates quirk-based visibility rules on the client const filteredComposition = useClientConditionsComposition(composition); return ( <UniformComposition data={filteredComposition} resolveRenderer={resolveRenderer} /> ); }

This hook uses immer internally for immutable updates and reads the current visitor's quirks from the Uniform Context to evaluate visibility conditions.


The Uniform visual editor allows content authors to edit compositions directly in the browser. The React SDK supports this with:

  1. Contextual editing -- Live preview updates when editing in the Uniform dashboard
  2. Inline editing -- Direct text editing within the rendered page (via UniformText and UniformRichText)
  3. Component selection -- Click-to-select components in the visual editor

To enable contextual editing, pass the contextualEditingEnhancer prop to UniformComposition:

<UniformComposition data={composition} resolveRenderer={resolveRenderer} contextualEditingEnhancer={async (composition) => { // Optional: apply data enhancers or transformations return composition; }} />

The SDK automatically:

  • Detects when the page is loaded inside the Uniform visual editor
  • Sets up a message channel for live composition updates
  • Adds data attributes to components and parameters for editor interaction
  • Provides the isContextualEditing flag via useUniformCurrentComposition

To enable the Uniform visual editor (contextual editing) with a React app, your server must handle two special requests from the Uniform dashboard:

  1. Config check -- The editor pings your preview URL to check capabilities (e.g., whether a playground route exists).
  2. Preview route -- When a content author opens the visual editor, the dashboard loads your app inside an iframe. On preview requests, your server should skip fetching composition data and instead return a page with an empty composition stub -- the editor sends the actual composition data over a postMessage channel.

Here is a complete Express server setup demonstrating both (from the react-vite-ssr starter):

// server.js import express from "express"; import cors from "cors"; import { isAllowedReferrer, EMPTY_COMPOSITION, IN_CONTEXT_EDITOR_CONFIG_CHECK_QUERY_STRING_PARAM, } from "@uniformdev/canvas"; import { getComposition } from "./src/uniform/api.js"; const app = express(); // CORS is required so the Uniform dashboard can load your app in an iframe app.use(cors()); app.use("*", async (req, res) => { // 1. Config check: the editor pings this to detect capabilities const isConfigCheck = req.query[IN_CONTEXT_EDITOR_CONFIG_CHECK_QUERY_STRING_PARAM] === "true"; if (isConfigCheck) { res.json({ hasPlayground: true }); return; } // 2. Preview mode: when loaded inside the Uniform visual editor let composition; if ( req.params[0] === "/api/preview" && isAllowedReferrer(req.headers.referer) ) { // No need to fetch -- the editor sends composition data via postMessage. // Use the empty stub so the page can render and establish the channel. composition = EMPTY_COMPOSITION; } else { // Normal request: fetch the composition from Uniform composition = await getComposition(req.params[0]); } // Server-render the app with the composition data const rendered = await render({ composition }); const html = template .replace("<!--app-html-->", rendered.html ?? ""); res.status(200).set({ "Content-Type": "text/html" }).send(html); });

note

isAllowedReferrer checks that the request originates from the Uniform dashboard domain. EMPTY_COMPOSITION is a minimal valid composition stub that lets the page render before the editor injects the real data. Both are exported from @uniformdev/canvas.

After setting up your server, configure the preview URL in your Uniform project so the visual editor knows where to load your app:

  1. Go to Settings > Canvas Settings > Preview in your Uniform project.
  2. Add a preview URL pointing to your preview route, for example: http://localhost:5173/api/preview.
  3. Optionally configure preview viewports (Desktop, Tablet, Mobile) for responsive previewing.

The react-vite-ssr starter includes a pre-configured preview URL and viewport definitions in the uniform-data/ folder that are pushed to your project with npm run uniform:push.

UniformPlayground is a special component for previewing individual components and patterns in the Uniform dashboard. It renders with an empty composition and supports contextual editing:

import { UniformPlayground } from "@uniformdev/canvas-react"; function PlaygroundPage() { return ( <UniformPlayground resolveRenderer={resolveRenderer} /> ); }

Context is Uniform's personalization and optimization engine. The @uniformdev/context-react package provides React components and hooks that integrate the Context engine into your React application, enabling personalization, A/B testing, visitor scoring, and behavior tracking.

You don't need Canvas compositions to use Uniform personalization. The @uniformdev/context-react package works standalone in any React app -- you can add personalization, A/B testing, and visitor scoring to an existing React application without adopting Canvas or changing your rendering architecture.

The react-starter-cra starter demonstrates this approach with a client-side React app that uses React Router for page navigation and Uniform Context for personalization. No Canvas compositions, no @uniformdev/canvas packages -- just Context.

// App.tsx -- Context-only setup with React Router import { UniformContext, useUniformContext } from "@uniformdev/context-react"; import { createUniformContext } from "./uniform/uniformContext"; import { Routes, Route, useLocation } from "react-router-dom"; import { useEffect } from "react"; import { parse } from "cookie"; const clientContext = createUniformContext(); function App() { return ( <UniformContext context={clientContext}> <AppRoutes /> </UniformContext> ); } function AppRoutes() { const { context } = useUniformContext(); const location = useLocation(); // Update Context on every route change with current URL and cookies useEffect(() => { context.update({ url: new URL( `${document.location.protocol}//${document.location.host}${location.pathname}${location.search}` ), cookies: parse(document.cookie ?? ""), }); }, [location, context]); return ( <Routes> <Route path="/" element={<HomePage />} /> <Route path="/developers" element={<DevelopersPage />} /> <Route path="/marketers" element={<MarketersPage />} /> </Routes> ); }

In this pattern, you track enrichments programmatically inside your page components:

// DevelopersPage.tsx import { useUniformContext } from "@uniformdev/context-react"; import { useEffect } from "react"; export default function DevelopersPage() { const { context } = useUniformContext(); useEffect(() => { context.update({ enrichments: [ { cat: "audience", key: "dev", str: 10, }, ], }); }, [context]); return <h2>For Developers</h2>; }

This approach is ideal when you want to:

  • Add personalization to an existing React SPA without a CMS
  • Use Uniform signals and scoring with your own component architecture
  • Implement programmatic enrichment tracking based on user navigation patterns

The UniformContext provider wraps your application and provides the Context instance to all child components:

import { Context, ManifestV2 } from "@uniformdev/context"; import { UniformContext } from "@uniformdev/context-react"; // The manifest defines your signals, enrichments, quirks, and tests. // In production, fetch this from the Uniform API or embed it at build time. const manifest: ManifestV2 = { project: { pz: { sig: { // Signal definitions }, }, test: { // A/B test definitions }, }, }; const context = new Context({ manifest, defaultConsent: true, }); function App({ children }) { return ( <UniformContext context={context}> {children} </UniformContext> ); }

By default, visitor scores and quirks are stored in memory and lost on page reload. Use CookieTransitionDataStore to persist visitor data across sessions via cookies:

import { Context, CookieTransitionDataStore, } from "@uniformdev/context"; const context = new Context({ manifest, defaultConsent: true, transitionStore: new CookieTransitionDataStore({ // For SSR, pass the cookie value from the server request headers: serverCookieValue: serverCookieValue ?? "", }), });

tip

For SSR apps, read the ufvd cookie from the incoming request headers and pass it as serverCookieValue. This ensures the server-rendered HTML reflects the visitor's existing scores and quirks.

Use enableDebugConsoleLogDrain during development to log all Context activity (signal evaluations, score changes, enrichment tracking) to the browser console:

import { Context, enableContextDevTools, enableDebugConsoleLogDrain, } from "@uniformdev/context"; const context = new Context({ manifest, defaultConsent: true, plugins: [ enableContextDevTools(), enableDebugConsoleLogDrain("debug"), ], });
PropTypeRequiredDescription
contextContextYesA configured Uniform Context instance
outputType'standard' | 'edge'NoRendering mode (default: 'standard'). Use 'edge' for edge-side personalization
trackRouteOnRenderbooleanNoWhether to track route changes on render (default: true)
includeTransferState'always' | 'never' | 'server-only'NoWhen to include server-to-client state transfer for SSR hydration (default: 'server-only')

When server-rendering, the UniformContext automatically injects a <script> tag containing the serialized context state. The client picks this up during hydration so that the visitor's scores and quirks are preserved without a flash of unpersonalized content:

// On the server, this injects transfer state automatically <UniformContext context={context} includeTransferState="always"> <App /> </UniformContext>

The personalization manifest defines your project's signals, enrichments, quirks, and A/B tests. There are several ways to create it:

Option 1: Hardcode for development

import { ManifestV2 } from "@uniformdev/context"; export const manifest: ManifestV2 = { project: { pz: { sig: { launchCampaign: { str: 50, cap: 100, dur: "p", crit: { op: "&", type: "G", clauses: [ { type: "QS", match: { cs: false, op: "=", rhs: "launch" }, queryName: "utm_campaign", }, ], }, }, }, }, test: {}, }, };

Option 2: Download via CLI (recommended for production)

npx uniform context manifest download --output ./src/uniform/manifest.json

Then import it:

import manifest from "./uniform/manifest.json";

Option 3: Fetch at runtime

import { ManifestClient } from "@uniformdev/context"; const manifestClient = new ManifestClient({ apiKey: process.env.UNIFORM_API_KEY, projectId: process.env.UNIFORM_PROJECT_ID, }); const manifest = await manifestClient.get();

warning

After modifying signals, enrichments, or quirks in the Uniform dashboard, you must publish the manifest for changes to take effect:

npx uniform context manifest publish

The Personalize component selects and renders content variants based on the visitor's scores, quirks, and other classification data.

  1. You define multiple variations of a content component, each tagged with personalization criteria (enrichment tags/scores).
  2. The Personalize component calls context.personalize() to evaluate the visitor's current scores against each variation's criteria.
  3. The winning variation(s) are rendered. If no variation matches, the default (first) variation is shown.
import { Personalize } from "@uniformdev/context-react"; const variations = [ { id: "default", title: "Welcome!", pz: {} }, { id: "tech-lover", title: "Welcome, tech enthusiast!", pz: { crit: [{ l: "tech", op: ">=", rhs: 50 }] }, }, { id: "marketer", title: "Welcome, marketing pro!", pz: { crit: [{ l: "marketing", op: ">=", rhs: 50 }] }, }, ]; function PersonalizedHero() { return ( <Personalize name="heroPersonalization" variations={variations} component={({ title, personalizationResult }) => ( <section> <h1>{title}</h1> {personalizationResult.personalizationOccurred && ( <span className="text-sm text-green-600"> Personalized for you </span> )} </section> )} /> ); }
PropTypeRequiredDescription
namestringYesUnique name for this placement (used in analytics)
variationsPersonalizedVariant[]YesArray of possible variations to select from
componentReact.ComponentTypeYesComponent to render for the selected variation
countnumberNoNumber of variations to select (default: 1)
algorithmstringNoPersonalization algorithm to use
wrapperComponentReact.ComponentTypeNoWrapper component that receives personalizationOccurred flag
compositionMetadataCompositionMetadataNoMetadata for analytics tracking

The rendered component receives all variation properties plus a personalizationResult object:

{ variation: PersonalizedVariant; // The selected variation data personalizationOccurred: boolean; // Whether personalization actually changed the output }

Use wrapperComponent to add conditional styling or tracking based on whether personalization occurred:

const PersonalizationWrapper = ({ children, personalizationOccurred }) => ( <div className={personalizationOccurred ? "border-l-4 border-blue-500" : ""} data-personalized={personalizationOccurred} > {children} </div> ); <Personalize name="hero" variations={variations} component={HeroVariant} wrapperComponent={PersonalizationWrapper} />

The Test component implements A/B testing by selecting a single variant based on test distribution percentages:

import { Test } from "@uniformdev/context-react"; const variations = [ { id: "control", testDistribution: 50, heading: "Sign up now" }, { id: "variant-a", testDistribution: 50, heading: "Get started free" }, ]; function ABTestHero() { return ( <Test name="signupHeadingTest" variations={variations} component={({ heading }) => <h1>{heading}</h1>} /> ); }
PropTypeRequiredDescription
namestringYesUnique test name
variationsTestVariant[]YesArray of test variations, each with a testDistribution percentage
componentReact.ComponentTypeYesComponent to render for the selected variation
loadingMode'default' | 'none' | React.ComponentTypeNoHow to handle loading state
compositionMetadataCompositionMetadataNoMetadata for analytics tracking

note

Key difference from personalization: A/B tests use deterministic distribution percentages (testDistribution) to split traffic. Personalization uses visitor scores and quirks to algorithmically select the best variant.


Track visitor interactions to build up enrichment scores that drive personalization. The SDK provides two tracking components:

Track tracks visitor behavior when content enters the viewport using IntersectionObserver:

import { Track } from "@uniformdev/context-react"; const Article = ({ enrichments, title, children }) => { return ( <Track behavior={enrichments}> <article> <h2>{title}</h2> {children} </article> </Track> ); };
PropTypeRequiredDescription
behaviorEnrichmentData | EnrichmentData[]YesEnrichment data to track
childrenReact.ReactNodeYesContent to track visibility of
tagNamekeyof JSX.IntrinsicElementsNoWrapper element (default: 'div')
thresholdnumber | number[]NoVisibility threshold (default: 0.5 = 50% visible)
disableVisibilityTriggerbooleanNoDisable viewport tracking (falls back to immediate)

TrackFragment tracks immediately on page load / route change without adding a wrapper element:

import { TrackFragment } from "@uniformdev/context-react"; const BlogPost = ({ enrichments, children }) => { return ( <> <TrackFragment behavior={enrichments} /> {children} </> ); };

Both tracking components:

  • Track once per URL change per component instance (no duplicate tracking)
  • Do not track when inside a Personalize component (prevents double-counting)
  • Call context.update({ enrichments }) to update the visitor's scores

Accesses the Uniform Context instance from the nearest UniformContext provider:

import { useUniformContext } from "@uniformdev/context-react"; const ContextDebug = () => { const { context } = useUniformContext(); const handleReset = () => { context.forget(true); }; return <button onClick={handleReset}>Reset visitor data</button>; };

By default, this hook throws an error if no UniformContext provider is found. To make it optional:

const result = useUniformContext({ throwOnMissingProvider: false }); // result may be undefined if no provider exists

warning

The context.scores and context.quirks properties accessed directly from the Context instance are not reactive -- they don't trigger re-renders when they change. Use useScores() and useQuirks() for reactive access.

Provides reactive access to the visitor's current scores. The component re-renders automatically when scores change:

import { useScores } from "@uniformdev/context-react"; const ScoreBoard = () => { const scores = useScores(); return ( <div> <h3>Your interests:</h3> <ul> {Object.entries(scores) .filter(([, score]) => score > 0) .sort(([, a], [, b]) => b - a) .map(([signal, score]) => ( <li key={signal}> {signal}: {score} </li> ))} </ul> </div> ); };

Returns a ScoreVector -- an object mapping signal names to numeric score values.

Provides reactive access to the visitor's current quirks. Re-renders when quirks change:

import { useQuirks } from "@uniformdev/context-react"; const LocationBanner = () => { const quirks = useQuirks(); if (!quirks.country) return null; return ( <div className="bg-blue-50 p-4 rounded"> Welcome from {quirks.country}! </div> ); };

Returns a Quirks object -- key-value pairs representing visitor attributes.

Use the Context instance to set quirks from user interactions:

import { useUniformContext } from "@uniformdev/context-react"; const CountrySelector = () => { const { context } = useUniformContext(); const setCountry = (country: string) => { context.update({ quirks: { country }, }); }; return ( <select onChange={(e) => setCountry(e.target.value)}> <option value="">Select country</option> <option value="US">United States</option> <option value="CA">Canada</option> <option value="UK">United Kingdom</option> </select> ); };

Determines if the current component is inside a Personalize component:

import { useIsPersonalized } from "@uniformdev/context-react"; const ContentBlock = ({ children }) => { const isPersonalized = useIsPersonalized(); return ( <div data-personalized={isPersonalized}> {children} {isPersonalized && <span className="badge">Personalized</span>} </div> ); };

The Context React SDK supports edge-side personalization and testing. When outputType is set to 'edge', the Personalize and Test components emit all variations wrapped in special HTML tags. An edge worker (Cloudflare, Vercel, Netlify, Akamai) then selects the winning variant before delivering the page to the visitor:

<UniformContext context={context} outputType="edge"> <App /> </UniformContext>

In edge mode:

  • Personalize renders all variations with metadata tags for edge selection
  • Test renders all variations with test distribution metadata
  • The edge worker evaluates scores/quirks and strips non-winning variants from the HTML

See the edge-side personalization guide for detailed setup instructions.


Here is a complete example showing how to wire up both Canvas and Context in a React + Vite SSR project.

your-project/ ā”œā”€ā”€ src/ │ ā”œā”€ā”€ components/ │ │ ā”œā”€ā”€ Page.tsx │ │ ā”œā”€ā”€ Hero.tsx │ │ └── resolveRenderer.ts │ ā”œā”€ā”€ uniform/ │ │ ā”œā”€ā”€ api.ts │ │ └── manifest.ts │ ā”œā”€ā”€ App.tsx │ ā”œā”€ā”€ entry-client.tsx │ └── entry-server.tsx ā”œā”€ā”€ server.js ā”œā”€ā”€ package.json └── vite.config.ts
// src/App.tsx import { UniformComposition } from "@uniformdev/canvas-react"; import { UniformContext } from "@uniformdev/context-react"; import { Context } from "@uniformdev/context"; import { resolveRenderer } from "./components/resolveRenderer"; import { manifest } from "./uniform/manifest"; import type { RootComponentInstance } from "@uniformdev/canvas"; const context = new Context({ manifest, defaultConsent: true, }); type AppProps = { composition?: RootComponentInstance; }; export default function App({ composition }: AppProps) { // On the client, prefer preloaded data from SSR let data = composition; if (typeof window !== "undefined" && (window as any).__UNIFORM_DATA__) { data = (window as any).__UNIFORM_DATA__; } if (!data) { return <div>No composition found</div>; } return ( <UniformContext context={context}> <UniformComposition data={data} resolveRenderer={resolveRenderer} behaviorTracking="onLoad" /> {/* Transfer composition data to client for hydration */} <script dangerouslySetInnerHTML={{ __html: `window.__UNIFORM_DATA__ = ${JSON.stringify(data)};`, }} /> </UniformContext> ); }
// src/components/resolveRenderer.ts import type { ComponentInstance } from "@uniformdev/canvas"; import { DefaultNotImplementedComponent } from "@uniformdev/canvas-react"; import Page from "./Page"; import Hero from "./Hero"; export function resolveRenderer(component: ComponentInstance) { switch (component.type) { case "page": return Page; case "hero": return Hero; default: return DefaultNotImplementedComponent; } }
// src/components/Page.tsx import { UniformSlot } from "@uniformdev/canvas-react"; import type { ComponentProps } from "@uniformdev/canvas-react"; const Page = ({ component }: ComponentProps) => { return ( <div className="page"> <UniformSlot name="content" /> </div> ); }; export default Page;
// src/components/Hero.tsx import { UniformText, UniformRichText } from "@uniformdev/canvas-react"; import type { ComponentProps } from "@uniformdev/canvas-react"; type HeroProps = ComponentProps<{ title: string; description?: string; }>; const Hero = ({ title, description, component }: HeroProps) => { return ( <section className="hero"> <UniformText parameterId="title" as="h1" placeholder="Hero title goes here" /> <UniformRichText parameterId="description" placeholder="Hero description" /> </section> ); }; export default Hero;
// src/uniform/api.ts import { RouteClient } from "@uniformdev/canvas"; const routeClient = new RouteClient({ projectId: process.env.UNIFORM_PROJECT_ID!, apiKey: process.env.UNIFORM_API_KEY!, }); export async function getComposition(path: string) { const response = await routeClient.getRoute({ path: path || "/" }); if (response.type === "composition") { return response.compositionApiResponse.composition; } return null; }

Server entry#

// src/entry-server.tsx import React from "react"; import ReactDOMServer from "react-dom/server"; import App from "./App"; import type { RootComponentInstance } from "@uniformdev/canvas"; export async function render({ composition, }: { composition: RootComponentInstance; }) { const html = ReactDOMServer.renderToString( <React.StrictMode> <App composition={composition} /> </React.StrictMode> ); return { html }; }

Client entry#

// src/entry-client.tsx import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App"; ReactDOM.hydrateRoot( document.getElementById("root")!, <React.StrictMode> <App /> </React.StrictMode> );

ExportDescription
UniformCompositionRenders a full Canvas composition tree
UniformSlotRenders child components from a named slot
UniformTextRenders text parameters with inline editing support
UniformRichTextRenders rich text parameters
UniformPlaygroundPlayground for visual editing preview
DefaultNotImplementedComponentFallback for unmapped component types
registerUniformComponentRegisters a component in the component store
componentStoreResolverResolver function that uses the component store
useUniformCurrentCompositionHook: access composition context
useUniformCurrentComponentHook: access current component context
useUniformContextualEditingHook: enable contextual editing
useUniformContextualEditingStateHook: read contextual editing state
convertComponentToPropsHelper: convert component instance to flat props
getParameterAttributesHelper: get inline editing data attributes
ExportDescription
UniformContextProvider component for the Uniform Context engine
PersonalizeRenders personalized content based on visitor scores
TestRenders A/B test variations based on distribution
TrackTracks behavior when content enters the viewport
TrackFragmentTracks behavior on page load (no wrapper element)
useUniformContextHook: access the Context instance
useScoresHook: reactive visitor scores
useQuirksHook: reactive visitor quirks
useIsPersonalizedHook: check if inside a Personalize component
ExportDescription
RouteClientClient for resolving routes and fetching compositions
CanvasClientClient for direct composition API access
RootComponentInstanceType for composition root data
ComponentInstanceType for individual component data
EMPTY_COMPOSITIONMinimal composition stub for visual editor preview routes
isAllowedReferrerChecks if a request originates from the Uniform dashboard
IN_CONTEXT_EDITOR_CONFIG_CHECK_QUERY_STRING_PARAMQuery string parameter name used by the editor config check
ExportDescription
ContextThe core personalization and scoring engine
ManifestV2Type for the personalization manifest
enableContextDevToolsPlugin for enabling the Uniform DevTools browser extension
enableDebugConsoleLogDrainPlugin for logging all Context activity to the browser console
CookieTransitionDataStorePersists visitor scores and quirks across sessions via cookies

  1. Install packages:
npm install @uniformdev/canvas-react@latest @uniformdev/context-react@latest @uniformdev/canvas@latest @uniformdev/context@latest
  1. Create environment variables (.env):
UNIFORM_PROJECT_ID=your-project-id UNIFORM_API_KEY=your-api-key
  1. Create the manifest file. Download it via CLI:
npx uniform context manifest download --output ./src/uniform/manifest.json
  1. Create the API client (src/uniform/api.ts):
import { RouteClient } from "@uniformdev/canvas"; const routeClient = new RouteClient({ projectId: import.meta.env.VITE_UNIFORM_PROJECT_ID, apiKey: import.meta.env.VITE_UNIFORM_API_KEY, }); export async function getComposition(path: string) { const response = await routeClient.getRoute({ path: path || "/" }); if (response.type === "composition") { return response.compositionApiResponse.composition; } return null; }
  1. Create the component resolver (src/resolveRenderer.ts):
import { DefaultNotImplementedComponent } from "@uniformdev/canvas-react"; // Import your components here export function resolveRenderer(component) { switch (component.type) { // Map your Uniform component types to React components default: return DefaultNotImplementedComponent; } }
  1. Wrap your app with UniformContext and render compositions with UniformComposition:
import { Context } from "@uniformdev/context"; import { UniformContext } from "@uniformdev/context-react"; import { UniformComposition } from "@uniformdev/canvas-react"; import manifest from "./uniform/manifest.json"; import { resolveRenderer } from "./resolveRenderer"; const context = new Context({ manifest, defaultConsent: true }); function App({ composition }) { return ( <UniformContext context={context}> <UniformComposition data={composition} resolveRenderer={resolveRenderer} /> </UniformContext> ); }
  1. Add CLI scripts to package.json:
{ "scripts": { "uniform:push": "npx uniform sync push", "uniform:pull": "npx uniform sync pull", "uniform:publish": "npx uniform context manifest publish", "uniform:manifest": "npx uniform context manifest download --output ./src/uniform/manifest.json" } }
import { UniformSlot } from "@uniformdev/canvas-react"; import { useQuirks, useScores } from "@uniformdev/context-react"; const PersonalizedSection = ({ component }) => { const quirks = useQuirks(); const scores = useScores(); const greeting = quirks.country === "CA" ? "Welcome, eh!" : "Welcome!"; const topSignal = Object.entries(scores) .sort(([, a], [, b]) => b - a)[0]; return ( <section> <h2>{greeting}</h2> {topSignal && ( <p>We noticed you like {topSignal[0]} (score: {topSignal[1]})</p> )} <UniformSlot name="personalizedContent" /> </section> ); };
import { Track, TrackFragment } from "@uniformdev/context-react"; import { UniformText } from "@uniformdev/canvas-react"; // Track when an article section scrolls into view const ArticleSection = ({ enrichments, component }) => { return ( <Track behavior={enrichments} threshold={0.5} tagName="section" > <UniformText parameterId="title" as="h2" /> <UniformText parameterId="body" as="p" /> </Track> ); }; // Track immediately on page load (no wrapper element) const PageView = ({ enrichments, children }) => { return ( <> <TrackFragment behavior={enrichments} /> {children} </> ); };

In a client-side SPA with React Router, Uniform Context needs to be told about route changes so signals based on URL patterns can evaluate correctly. Update Context in a useEffect that depends on the current location:

import { useUniformContext } from "@uniformdev/context-react"; import { useLocation } from "react-router-dom"; import { parse } from "cookie"; import { useEffect } from "react"; function ContextRouteSync({ children }) { const { context } = useUniformContext(); const location = useLocation(); useEffect(() => { context.update({ url: new URL( `${document.location.protocol}//${document.location.host}${location.pathname}${location.search}` ), cookies: parse(document.cookie ?? ""), }); }, [location, context]); return <>{children}</>; }

Place this component just below UniformContext in your tree so all child routes benefit from the updated Context state.

Build a debug panel that shows the current visitor's scores and quirks during development. This is useful for verifying that signals fire correctly and enrichments accumulate as expected:

import { useUniformContext } from "@uniformdev/context-react"; function ContextDebugPanel() { const { context } = useUniformContext(); const forgetMe = async () => { await context.forget(true); window.location.reload(); }; return ( <div style={{ padding: 16, background: "#f5f5f5", fontSize: 12 }}> <h3>Context Debug</h3> <p>Quirks:</p> <pre>{JSON.stringify(context.quirks, null, 2)}</pre> <p>Scores:</p> <pre>{JSON.stringify(context.scores, null, 2)}</pre> <button onClick={forgetMe}>Reset visitor data</button> </div> ); }

tip

context.forget(true) clears all stored visitor data including scores, quirks, and consent state. The true argument also clears the cookie-based transition store if you are using CookieTransitionDataStore.

Enable the Uniform Context DevTools browser extension for debugging:

import { Context, enableContextDevTools } from "@uniformdev/context"; import { UniformContext } from "@uniformdev/context-react"; import manifest from "./uniform/manifest.json"; const context = new Context({ manifest, defaultConsent: true, plugins: [enableContextDevTools()], }); function App({ children }) { return ( <UniformContext context={context}> {children} </UniformContext> ); }

The DevTools extension lets you inspect and modify visitor scores and quirks in real time during development.


  1. Always wrap with UniformContext -- Place the UniformContext provider above UniformComposition in the component tree. Canvas components need Context for personalization, A/B testing, and behavior tracking.

  2. Create the Context instance once -- Instantiate the Context object at the module level or in a top-level component, not inside render functions. Creating it in a render function resets visitor state on every render.

  3. Use DefaultNotImplementedComponent as fallback -- Always return a fallback from your component resolver. It shows the component type name and a helpful code snippet during development.

  4. Mark parameter types as optional -- Even required parameters can be undefined at runtime (e.g., when first adding a component to a composition). Always handle undefined values.

  5. Use UniformText and UniformRichText for visible text -- These components provide inline editing support in the Uniform visual editor. Use raw parameter access only for non-visible values like URLs, booleans, or conditional logic.

  6. Hydrate composition data for SSR -- When server-rendering, serialize the composition to a <script> tag and read it on the client to avoid re-fetching during hydration.

  7. Keep the manifest up to date -- Run npx uniform context manifest publish after modifying signals, enrichments, or quirks. Then re-download or re-fetch the manifest.

  8. Use useScores and useQuirks for reactive data -- Accessing context.scores directly does not trigger re-renders. Always use the hooks for values that affect the UI.

  9. Avoid tracking inside Personalize -- The Track and TrackFragment components automatically skip tracking when inside a Personalize component to prevent double-counting.

  10. Leverage immer for visibility rules -- The useClientConditionsComposition hook requires immer >= 10 as a peer dependency. Install it if you use client-side visibility rules.