A/B testing is crucial for data-driven decision making in React applications. Here’s a comprehensive guide to implementing various A/B testing strategies:
Client-Side A/B Testing
1. Feature Flag Implementation
// features.js
export const featureFlags = {
newCheckoutDesign: {
enabled: Math.random() < 0.5, // 50% rollout
variant: Math.random() < 0.3 ? 'A' : 'B' // 30% get variant A
},
premiumUpsell: {
enabled: localStorage.getItem('userSegment') === 'premium'
}
};
// Usage in component
function CheckoutPage() {
if (featureFlags.newCheckoutDesign.enabled) {
return featureFlags.newCheckoutDesign.variant === 'A'
? <CheckoutDesignA />
: <CheckoutDesignB />;
}
return <LegacyCheckout />;
}
2. Optimizely Integration
// optimizely.js
import { createInstance } from '@optimizely/optimizely-sdk';
const optimizely = createInstance({
sdkKey: process.env.REACT_APP_OPTIMIZELY_SDK_KEY,
datafileOptions: {
autoUpdate: true,
updateInterval: 300000 // 5 minutes
}
});
export async function activateExperiment(userId, experimentKey) {
await optimizely.onReady();
const userContext = optimizely.createUserContext(userId);
const decision = userContext.decide(experimentKey);
return decision.variationKey;
}
// Usage
const variant = await activateExperiment('user123', 'checkout_experiment');
Server-Side A/B Testing
3. Next.js Middleware Variant Routing
// middleware.js
import { NextResponse } from 'next/server';
export function middleware(request) {
const url = request.nextUrl;
const cookie = request.cookies.get('ab-test-variant');
// Set variant if not already set
if (!cookie) {
const variant = Math.random() < 0.5 ? 'a' : 'b';
url.searchParams.set('variant', variant);
const response = NextResponse.redirect(url);
response.cookies.set('ab-test-variant', variant);
return response;
}
// Pass variant to page
url.searchParams.set('variant', cookie.value);
return NextResponse.rewrite(url);
}
4. Vercel Edge Config Testing
// lib/ab-testing.js
import { get } from '@vercel/edge-config';
export async function getVariant(userId, experimentName) {
const experiments = await get('experiments');
const experiment = experiments[experimentName];
if (!experiment) return 'control';
// Deterministic variant assignment based on user ID
const hash = hashCode(userId);
const percentage = hash % 100;
let cumulativePercentage = 0;
for (const [variant, percent] of Object.entries(experiment.variants)) {
cumulativePercentage += percent;
if (percentage < cumulativePercentage) {
return variant;
}
}
return 'control';
}
function hashCode(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = (hash << 5) - hash + str.charCodeAt(i);
hash |= 0; // Convert to 32bit integer
}
return Math.abs(hash);
}
Analytics and Tracking
5. Google Analytics Integration
// useABTest.js hook
import { useEffect } from 'react';
import ReactGA from 'react-ga4';
export function useABTest(experimentId, variant) {
useEffect(() => {
if (variant) {
ReactGA.gtag('event', 'experiment_impression', {
experiment_id: experimentId,
variant_id: variant
});
}
}, [experimentId, variant]);
}
// Component usage
function HeroBanner({ variant }) {
useABTest('hero_banner_test', variant);
return (
<div>
{variant === 'A' ? <HeroA /> : <HeroB />}
</div>
);
}
6. Custom Event Tracking
// tracking.js
export function trackExperimentEvent(experimentName, variant, eventName, metadata = {}) {
if (window.analytics) {
window.analytics.track('experiment_event', {
experiment: experimentName,
variant,
event: eventName,
...metadata,
timestamp: new Date().toISOString()
});
}
// Fallback to localStorage if analytics not loaded
const events = JSON.parse(localStorage.getItem('abTestEvents') || []);
events.push({
experimentName,
variant,
eventName,
metadata,
timestamp: new Date().toISOString()
});
localStorage.setItem('abTestEvents', JSON.stringify(events));
}
Advanced Techniques
7. Multi-Armed Bandit Implementation
// bandit.js
class Bandit {
constructor(variants) {
this.variants = variants.reduce((acc, variant) => {
acc[variant] = { wins: 0, trials: 0 };
return acc;
}, {});
}
selectVariant() {
// Epsilon-greedy strategy
const epsilon = 0.1;
if (Math.random() < epsilon) {
// Exploration: random variant
const keys = Object.keys(this.variants);
return keys[Math.floor(Math.random() * keys.length)];
} else {
// Exploitation: best performing variant
return Object.entries(this.variants).reduce(
(best, [variant, { wins, trials }]) => {
const rate = trials > 0 ? wins / trials : 0;
return rate > best.rate ? { variant, rate } : best;
},
{ variant: null, rate: -1 }
).variant;
}
}
recordWin(variant) {
if (this.variants[variant]) {
this.variants[variant].wins += 1;
this.variants[variant].trials += 1;
}
}
recordLoss(variant) {
if (this.variants[variant]) {
this.variants[variant].trials += 1;
}
}
}
// Singleton instance
export const bandit = new Bandit(['A', 'B', 'C']);
8. SSR A/B Testing with Cookies
// withABTest.js HOC
import { getCookie } from 'cookies-next';
export function withABTest(WrappedComponent, experimentName) {
return function ABTestWrapper(props) {
const variant = getCookie(`ab-${experimentName}`) || 'control';
return <WrappedComponent {...props} variant={variant} />;
};
}
// Server-side (Next.js)
export async function getServerSideProps({ req, res }) {
let variant = getCookie(`ab-checkout-test`, { req, res });
if (!variant) {
variant = Math.random() < 0.5 ? 'A' : 'B';
setCookie(`ab-checkout-test`, variant, { req, res, maxAge: 60 * 60 * 24 * 30 }); // 30 days
}
return { props: { variant } };
}
Testing Framework Integration
9. Jest Testing Utilities
// test-utils.js
export function mockFeatureFlags(flags) {
jest.mock('../features', () => ({
featureFlags: {
...jest.requireActual('../features').featureFlags,
...flags
}
}));
}
// Test usage
describe('CheckoutPage with feature flag', () => {
beforeEach(() => {
mockFeatureFlags({
newCheckoutDesign: {
enabled: true,
variant: 'B'
}
});
});
it('renders variant B', () => {
render(<CheckoutPage />);
expect(screen.getByTestId('checkout-design-b')).toBeInTheDocument();
});
});
10. Cypress A/B Testing Commands
// cypress/support/commands.js
Cypress.Commands.add('setABTestVariant', (experimentName, variant) => {
cy.setCookie(`ab-${experimentName}`, variant);
});
// Test usage
describe('Checkout A/B Test', () => {
it('shows variant A', () => {
cy.setABTestVariant('checkout_test', 'A');
cy.visit('/checkout');
cy.get('[data-testid="checkout-a"]').should('exist');
});
});
Best Practices Checklist
- Implementation Strategy
- [ ] Choose between client-side and server-side testing
- [ ] Ensure consistent variant assignment
- [ ] Implement proper user segmentation
- Analytics Integration
- [ ] Track experiment impressions
- [ ] Record conversion events
- [ ] Set up proper goal metrics
- Performance Considerations
- [ ] Minimize flickering between variants
- [ ] Cache variant assignments
- [ ] Avoid performance overhead
- Testing Framework
- [ ] Create test utilities for mocking variants
- [ ] Write integration tests for all variants
- [ ] Test analytics tracking
- Statistical Significance
- [ ] Determine required sample size
- [ ] Run tests for adequate duration
- [ ] Use proper statistical methods
- Privacy Compliance
- [ ] Document data collection
- [ ] Provide opt-out mechanisms
- [ ] Comply with GDPR/CCPA
- Documentation
- [ ] Document running experiments
- [ ] Track hypothesis and results
- [ ] Share learnings with team