Install
npx skillscat add twofoldtech-dakota/claude-marketplace/plugins-optimizely-experimentation-analyzer-skills-frontend-modern Install via the SkillsCat registry.
SKILL.md
Modern Frontend Patterns for Experimentation
Overview
This skill covers modern frontend patterns for implementing Optimizely Experimentation in React and Next.js applications.
Custom Hooks
useFeatureFlag Hook
import { useDecision } from '@optimizely/react-sdk';
interface FeatureFlagResult<T = Record<string, unknown>> {
isEnabled: boolean;
isReady: boolean;
variables: T;
}
function useFeatureFlag<T = Record<string, unknown>>(
flagKey: string
): FeatureFlagResult<T> {
const [decision, clientReady] = useDecision(flagKey);
return {
isReady: clientReady,
isEnabled: decision.enabled,
variables: decision.variables as T,
};
}
// Usage
interface DarkModeVariables {
theme: 'dark' | 'light' | 'system';
accentColor: string;
}
function ThemeProvider({ children }) {
const { isEnabled, isReady, variables } = useFeatureFlag<DarkModeVariables>('dark_mode');
if (!isReady) return <Loading />;
return (
<ThemeContext.Provider value={isEnabled ? variables : defaultTheme}>
{children}
</ThemeContext.Provider>
);
}useExperiment Hook
interface ExperimentResult {
variation: string;
isReady: boolean;
isControl: boolean;
}
function useExperiment(experimentKey: string): ExperimentResult {
const [decision, clientReady] = useDecision(experimentKey);
return {
isReady: clientReady,
variation: decision.variationKey || 'control',
isControl: decision.variationKey === 'control' || !decision.variationKey,
};
}
// Usage
function CheckoutPage() {
const { variation, isReady, isControl } = useExperiment('checkout_flow');
if (!isReady) return <CheckoutSkeleton />;
if (isControl) return <StandardCheckout />;
return variation === 'express'
? <ExpressCheckout />
: <SinglePageCheckout />;
}Next.js Integration
App Router Provider
// app/providers.tsx
'use client';
import { OptimizelyProvider, createInstance } from '@optimizely/react-sdk';
import { useEffect, useState } from 'react';
const optimizely = createInstance({
sdkKey: process.env.NEXT_PUBLIC_OPTIMIZELY_SDK_KEY!,
});
export function ExperimentationProvider({
children,
userId,
}: {
children: React.ReactNode;
userId: string;
}) {
const [isReady, setIsReady] = useState(false);
useEffect(() => {
optimizely.onReady().then(() => setIsReady(true));
}, []);
return (
<OptimizelyProvider
optimizely={optimizely}
user={{ id: userId }}
timeout={500}
>
{children}
</OptimizelyProvider>
);
}Server Components with Experimentation
// app/page.tsx
import { headers } from 'next/headers';
import { getOptimizelyDecision } from '@/lib/optimizely-server';
export default async function Page() {
const headersList = headers();
const userId = headersList.get('x-user-id') || 'anonymous';
const decision = await getOptimizelyDecision('hero_experiment', userId);
return (
<main>
{decision.variationKey === 'new_hero'
? <NewHero />
: <StandardHero />}
</main>
);
}Testing Patterns
Mock Setup
// __mocks__/@optimizely/react-sdk.ts
export const useDecision = jest.fn();
export const OptimizelyProvider = ({ children }: { children: React.ReactNode }) => children;
export const createInstance = jest.fn();Test Variations
import { render, screen } from '@testing-library/react';
import { useDecision } from '@optimizely/react-sdk';
import { CheckoutPage } from './CheckoutPage';
jest.mock('@optimizely/react-sdk');
describe('CheckoutPage', () => {
it('renders control checkout', () => {
(useDecision as jest.Mock).mockReturnValue([
{ variationKey: 'control', enabled: true, variables: {} },
true,
]);
render(<CheckoutPage />);
expect(screen.getByText('Standard Checkout')).toBeInTheDocument();
});
it('renders express checkout variation', () => {
(useDecision as jest.Mock).mockReturnValue([
{ variationKey: 'express', enabled: true, variables: {} },
true,
]);
render(<CheckoutPage />);
expect(screen.getByText('Express Checkout')).toBeInTheDocument();
});
it('shows loading while SDK initializes', () => {
(useDecision as jest.Mock).mockReturnValue([
{ variationKey: null, enabled: false, variables: {} },
false,
]);
render(<CheckoutPage />);
expect(screen.getByTestId('checkout-skeleton')).toBeInTheDocument();
});
});Error Boundaries
class ExperimentErrorBoundary extends React.Component<
{ children: React.ReactNode; fallback: React.ReactNode },
{ hasError: boolean }
> {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error: Error, info: React.ErrorInfo) {
console.error('Experiment error:', error, info);
}
render() {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
}
// Usage
<ExperimentErrorBoundary fallback={<ControlComponent />}>
<ExperimentComponent />
</ExperimentErrorBoundary>Best Practices
- Create typed custom hooks for consistency
- Handle loading states with skeletons
- Use error boundaries for resilience
- Test all variations thoroughly
- Memoize decisions when not using hooks