Experiments

Run A/B experiments with deterministic, SSR-safe user assignment and percentage-based bucketing.

Defining Experiments

const config = defineAccess({
  // ...
  experiments: {
    "checkout-flow": {
      active: true,
      variants: ["control", "optimized", "minimal"],
      defaultVariant: "control",
      allocation: { control: 50, optimized: 30, minimal: 20 },
    },
    "pricing-page": {
      active: true,
      variants: ["current", "new-layout"],
      defaultVariant: "current",
      // Without allocation: even 50/50 split
    },
    "old-experiment": {
      active: false,
      variants: ["a", "b"],
      defaultVariant: "a",
      // Inactive: all users get defaultVariant
    },
  },
});

Experiment Definition

FieldTypeDescription
activebooleanWhether the experiment is running
variantsstring[]List of variant names
defaultVariantstringFallback variant when inactive
allocationRecord<string, number>Percentage per variant (must sum to 100)

Assignment Mechanism

  • Uses a deterministic hash of userId + experimentId
  • SSR-safe — produces identical results on server and client
  • Without allocation: variants are distributed evenly
  • With allocation: percentage-based bucketing
User hash: 0────────50────────80──────100
           │ control │ optimized│ minimal│

When an experiment is inactive, all users receive the defaultVariant with active: false.

Using in Components

<Experiment> Component

import { Experiment } from "react-access-engine";

<Experiment
  id="checkout-flow"
  variants={{
    control: <ClassicCheckout />,
    optimized: <OptimizedCheckout />,
    minimal: <MinimalCheckout />,
  }}
  fallback={<ClassicCheckout />}
/>;

The component renders the variant assigned to the current user. If the experiment is inactive or not found, it renders the fallback.

useExperiment Hook

import { useExperiment } from "react-access-engine";

function CheckoutPage() {
  const { experimentId, variant, active } = useExperiment("checkout-flow");

  // Track exposure
  useEffect(() => {
    if (active) {
      analytics.track("experiment_exposure", { experimentId, variant });
    }
  }, [experimentId, variant, active]);

  switch (variant) {
    case "optimized":
      return <OptimizedCheckout />;
    case "minimal":
      return <MinimalCheckout />;
    default:
      return <ClassicCheckout />;
  }
}

Tracking with Plugins

Use the analytics plugin to automatically track experiment assignments:

import { createAnalyticsPlugin } from "react-access-engine";

const analyticsPlugin = createAnalyticsPlugin({
  adapter: {
    track: (event, properties) => {
      // Send to your analytics provider
      mixpanel.track(event, properties);
    },
  },
  trackExperiments: true,
});

const config = defineAccess({
  // ...
  plugins: [analyticsPlugin],
  experiments: {
    /* ... */
  },
});

Events emitted:

  • rac:experiment_assign — when a user is assigned to a variant

Even vs Custom Allocation

Even Distribution (default)

experiments: {
  'test-1': {
    active: true,
    variants: ['a', 'b', 'c'],
    defaultVariant: 'a',
    // Each variant gets ~33.3% of traffic
  },
}

Custom Allocation

experiments: {
  'test-2': {
    active: true,
    variants: ['control', 'treatment'],
    defaultVariant: 'control',
    allocation: {
      control: 90,    // 90% see control
      treatment: 10,  // 10% see treatment
    },
  },
}

The allocation values must sum to 100.

Deactivating Experiments

Set active: false to gracefully deactivate. All users will receive the defaultVariant:

experiments: {
  'finished-experiment': {
    active: false,
    variants: ['control', 'winner'],
    defaultVariant: 'winner', // Winner gets rolled out to 100%
  },
}