The short answer: Use React Context to provide experiment variants app-wide, and a custom hook (useExperiment) to access them. For Next.js, assign variants in middleware to avoid flicker. For client-only React, initialize experiments before ReactDOM.render().
Who this is for
- React developers implementing A/B testing
- Teams building experimentation into React apps
- Anyone migrating from a visual editor to code-based testing
Who this is NOT for
- Non-developers (use visual editor tools)
- Next.js App Router users (see our Next.js guide)
Approaches Compared
| Approach | Difficulty | Flicker | Best For |
|---|---|---|---|
| Context + Hook | Easy | Possible | Simple React apps |
| Server-Side (Next.js) | Medium | None | Next.js apps |
| Feature Flag Service | Easy | None | Teams using feature flags |
React Context + Hook Implementation
This is the simplest approach for React apps. Create a context provider and custom hook.
1. Create the Experiment Context
// contexts/ExperimentContext.tsx
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
type Variant = string;
type Experiments = Record<string, Variant>;
interface ExperimentContextType {
experiments: Experiments;
getVariant: (experimentId: string) => Variant;
isLoading: boolean;
}
const ExperimentContext = createContext<ExperimentContextType | undefined>(undefined);
// Simple hash function for consistent assignment
function hashString(str: string): number {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return Math.abs(hash);
}
function assignVariant(userId: string, experimentId: string, variants: string[]): string {
const hash = hashString(`${userId}-${experimentId}`);
const index = hash % variants.length;
return variants[index];
}
interface ExperimentProviderProps {
children: ReactNode;
userId: string;
experiments: Record<string, string[]>; // experimentId -> variants
}
export function ExperimentProvider({
children,
userId,
experiments: experimentConfig
}: ExperimentProviderProps) {
const [experiments, setExperiments] = useState<Experiments>({});
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// Assign variants for all experiments
const assigned: Experiments = {};
for (const [experimentId, variants] of Object.entries(experimentConfig)) {
// Check for existing assignment in localStorage
const storageKey = `exp_${experimentId}`;
let variant = localStorage.getItem(storageKey);
if (!variant) {
variant = assignVariant(userId, experimentId, variants);
localStorage.setItem(storageKey, variant);
}
assigned[experimentId] = variant;
}
setExperiments(assigned);
setIsLoading(false);
}, [userId, experimentConfig]);
const getVariant = (experimentId: string): Variant => {
return experiments[experimentId] || 'control';
};
return (
<ExperimentContext.Provider value=>
{children}
</ExperimentContext.Provider>
);
}
export function useExperiment(experimentId: string): Variant {
const context = useContext(ExperimentContext);
if (!context) {
throw new Error('useExperiment must be used within ExperimentProvider');
}
return context.getVariant(experimentId);
}
export function useExperiments() {
const context = useContext(ExperimentContext);
if (!context) {
throw new Error('useExperiments must be used within ExperimentProvider');
}
return context;
}2. Wrap Your App
// App.tsx
import { ExperimentProvider } from './contexts/ExperimentContext';
const EXPERIMENTS = {
'hero-headline': ['control', 'variant-a', 'variant-b'],
'pricing-layout': ['control', 'horizontal'],
};
function App() {
// Get or create user ID
const userId = localStorage.getItem('userId') || crypto.randomUUID();
return (
<ExperimentProvider userId={userId} experiments={EXPERIMENTS}>
<YourApp />
</ExperimentProvider>
);
}3. Use in Components
// components/Hero.tsx
import { useExperiment } from '../contexts/ExperimentContext';
const headlines = {
'control': 'Build Better Products',
'variant-a': 'Increase Conversions by 30%',
'variant-b': 'Stop Guessing. Start Testing.',
};
export function Hero() {
const variant = useExperiment('hero-headline');
const headline = headlines[variant as keyof typeof headlines];
return (
<section>
<h1>{headline}</h1>
{/* Track impression */}
<TrackImpression experiment="hero-headline" variant={variant} />
</section>
);
}4. Track Conversions
// utils/tracking.ts
export function trackConversion(
experimentId: string,
variant: string,
event: string
) {
// Send to your analytics
fetch('/api/track', {
method: 'POST',
body: JSON.stringify({
experiment: experimentId,
variant,
event,
timestamp: Date.now(),
}),
});
// Or use ExperimentHQ
window.experimenthq?.track(event, {
experiment: experimentId,
variant,
});
}
// Usage in component
function CTAButton() {
const variant = useExperiment('hero-headline');
const handleClick = () => {
trackConversion('hero-headline', variant, 'cta_click');
// ... rest of click handler
};
return <button onClick={handleClick}>Get Started</button>;
}Avoiding Flicker in React
The context approach above can cause flicker because variants are assigned after the component mounts. To prevent this:
Option 1: Initialize Before Render
Assign variants in a script that runs before React, then read from localStorage in your context.
Option 2: Server-Side Assignment
For Next.js, use middleware to assign variants. See our Next.js guide.
Option 3: Loading State
Show a loading skeleton until variants are assigned. Not ideal but simple.
Common Mistakes
❌ Assigning variants on every render
Users must see the same variant consistently. Always persist to localStorage or cookies.
❌ Using Math.random() for assignment
Math.random() gives different results on each call. Use a hash function with user ID for deterministic assignment.
❌ Not handling SSR/hydration
If using SSR, ensure server and client assign the same variant to avoid hydration mismatches.
Easier Option: Use ExperimentHQ
Building your own experimentation system is complex. ExperimentHQ handles:
- Consistent variant assignment across sessions
- Statistical significance calculations
- Visual editor for non-developers
- Anti-flicker technology