useXafExperiment
useXafExperiment
Lightweight A/B testing composable. Assigns the current visitor to a variant of a named experiment, persists the assignment in localStorage (so repeat visits see the same variant), and exposes a track() helper that dispatches a xaf:experiment:exposed window event for analytics.
The assignment is sticky for the experiment's TTL. After that, a new random assignment is made — useful for experiments that should be re-randomized after a few weeks.
Variant weights are normalised at runtime, so they don't have to sum to 1, but they must be non-negative. A weight of 0 means "never assign" (the variant is in the list but won't be picked).
Usage
const { variant, isReady, track, reset } = useXafExperiment({
id: 'homepage_hero_v2',
variants: [
{ id: 'control', weight: 1 },
{ id: 'treatment', weight: 1 },
],
})
// In template:
<div v-if="variant === 'treatment'">New hero design</div>
// Track an exposure:
onMounted(() => track('view'))
Options
| Option | Type | Default | Description |
|---|---|---|---|
id | string | required | Experiment id — also used as the localStorage key (xaf:exp:{id}). |
variants | XafExperimentVariant[] | required | Variants to randomly assign between. Each has id and optional weight (default 1). |
ttlDays | number | 30 | TTL in days before re-randomizing the visitor's assignment. |
userId | string | — | When provided, deterministic bucketing via djb2 hash so the same visitor always sees the same variant across sessions/devices. |
force | string | — | Force a specific variant (bypasses random assignment). Useful for QA tools and preview environments. |
Return value
| Field | Type | Description |
|---|---|---|
variant | Ref<string | null> | The variant id assigned to this visitor. null until isReady. |
isReady | Ref<boolean> | true once the assignment has been computed. |
track | (eventName?: string) => void | Emit an exposure event. Safe to call before isReady (becomes a no-op). |
reset | () => void | Manually re-assign the variant (drops the persisted assignment and re-picks). |
Server-side rendering
On the server, useXafExperiment() does not read localStorage. It falls back to:
forceif set, otherwise- a deterministic pick via
userId(if provided), otherwise - the first variant.
This avoids hydration mismatches: the client picks deterministically when userId is set, or randomly otherwise. Don't depend on a specific variant being assigned on the server — guard important UI with isReady.
Deterministic bucketing
When userId is provided, the same user always lands on the same variant (modulo TTL). This is useful for:
- Cross-device experiments (when
userIdis the logged-in user's id) - Server-rendered previews (the server picks the same variant as the client will)
The hash function is djb2, applied mod the sum of weights.
Exposure events
track() dispatches a CustomEvent on window:
window.addEventListener('xaf:experiment:exposed', (e) => {
const { id, variant, event, ts } = (e as CustomEvent).detail
// Push to your analytics provider (GA4, Plausible, etc.)
})
Default eventName is 'exposed'. Pass a custom name ('click', 'conversion') for funnel analysis.
Tips
- Always call
track()inonMounted(notsetup) —localStorageis only available on the client. - Pass
forcein dev/preview environments so designers see consistent results:const { variant } = useXafExperiment({ id: 'homepage_hero_v2', variants: [...], force: import.meta.dev ? 'treatment' : undefined, }) - Use
useStateor a Pinia store to sharevariantacross components —useXafExperiment()is independent per call, so wrapping it in a shared composable avoids divergent assignments.
AI Context
composable: useXafExperiment
package: "@xenterprises/nuxt-x-affiliate"
use-when: A/B testing, feature flagging, or experiment assignment with sticky persistence
Affiliate Impression
IntersectionObserver-based impression tracking — fires affiliate_impression to dataLayer when a buy button enters the viewport. Measure view-through conversion.
useXafLocale
Reactive locale state composable. Reads from a parent XAFLocaleProvider via inject, falls back to a local singleton. Includes a small t() helper for tiny strings.
