Savvi Studio

Decorators Reference

Last Updated: November 23, 2025

This document covers decorator patterns for wrapping stories with context providers, authentication states, and other wrappers.


Table of Contents

  1. Decorator Basics
  2. Authentication Decorators
  3. Provider Decorators
  4. Decorator Composition
  5. Global vs Story-Level Decorators
  6. Best Practices

Decorator Basics

Decorators wrap stories with additional functionality or context.

Basic Decorator Pattern

import type { Decorator } from '@storybook/react';

const withPadding: Decorator = (Story) => (
  <div style={{ padding: '20px' }}>
    <Story />
  </div>
);

export const Example: Story = {
  decorators: [withPadding],
};

Multiple Decorators

Decorators are applied from inside out (last to first in array):

const withPadding: Decorator = (Story) => (
  <div style={{ padding: '20px' }}>
    <Story />
  </div>
);

const withBorder: Decorator = (Story) => (
  <div style={{ border: '1px solid red' }}>
    <Story />
  </div>
);

export const Example: Story = {
  decorators: [
    withPadding,  // Applied first (innermost)
    withBorder,   // Applied second (outermost)
  ],
};

Authentication Decorators

MockAuthProvider Decorator

Wrap stories with authentication context:

import { MockAuthProvider, AuthStates } from '@/tests/mocks/providers/MockAuthProvider';

const withAuth: Decorator = (Story) => (
  <MockAuthProvider {...AuthStates.authenticated}>
    <Story />
  </MockAuthProvider>
);

export const Protected: Story = {
  decorators: [withAuth],
};

Predefined Auth States

// Authenticated user
const withAuth: Decorator = (Story) => (
  <MockAuthProvider {...AuthStates.authenticated}>
    <Story />
  </MockAuthProvider>
);

// Admin user
const withAdmin: Decorator = (Story) => (
  <MockAuthProvider {...AuthStates.admin}>
    <Story />
  </MockAuthProvider>
);

// Unauthenticated
const withNoAuth: Decorator = (Story) => (
  <MockAuthProvider {...AuthStates.unauthenticated}>
    <Story />
  </MockAuthProvider>
);

Custom Auth States

const withCustomAuth: Decorator = (Story) => (
  <MockAuthProvider
    isAuthenticated={true}
    user={{
      id: 'custom-user-123',
      email: 'custom@example.com',
      name: 'Custom User',
      role: 'manager',
    }}
  >
    <Story />
  </MockAuthProvider>
);

Provider Decorators

React Query Provider

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const withQueryClient: Decorator = (Story) => {
  const queryClient = new QueryClient({
    defaultOptions: {
      queries: { retry: false },
    },
  });
  
  return (
    <QueryClientProvider client={queryClient}>
      <Story />
    </QueryClientProvider>
  );
};

Theme Provider

import { ThemeProvider } from '@/providers/ThemeProvider';

const withTheme: Decorator = (Story) => (
  <ThemeProvider defaultTheme="light">
    <Story />
  </ThemeProvider>
);

Router Provider

import { BrowserRouter } from 'react-router-dom';

const withRouter: Decorator = (Story) => (
  <BrowserRouter>
    <Story />
  </BrowserRouter>
);

Multiple Providers

const withProviders: Decorator = (Story) => (
  <QueryClientProvider client={queryClient}>
    <ThemeProvider>
      <MockAuthProvider {...AuthStates.authenticated}>
        <BrowserRouter>
          <Story />
        </BrowserRouter>
      </MockAuthProvider>
    </ThemeProvider>
  </QueryClientProvider>
);

Decorator Composition

Reusable Decorator Factory

Create decorators that accept configuration:

const withAuthState = (state: typeof AuthStates[keyof typeof AuthStates]): Decorator => 
  (Story) => (
    <MockAuthProvider {...state}>
      <Story />
    </MockAuthProvider>
  );

// Usage
export const AsAdmin: Story = {
  decorators: [withAuthState(AuthStates.admin)],
};

export const AsUser: Story = {
  decorators: [withAuthState(AuthStates.authenticated)],
};

Conditional Decorators

const withConditionalAuth = (requireAuth: boolean): Decorator => 
  (Story) => {
    if (!requireAuth) {
      return <Story />;
    }
    return (
      <MockAuthProvider {...AuthStates.authenticated}>
        <Story />
      </MockAuthProvider>
    );
  };

Composing Decorators

const composeDecorators = (...decorators: Decorator[]): Decorator =>
  (Story) => decorators.reduceRight(
    (acc, decorator) => () => decorator(acc),
    Story
  )();

const authAndTheme = composeDecorators(withAuth, withTheme);

export const Example: Story = {
  decorators: [authAndTheme],
};

Global vs Story-Level Decorators

Global Decorators

Configure in .storybook/preview.tsx:

import type { Preview } from '@storybook/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient();

const preview: Preview = {
  decorators: [
    (Story) => (
      <QueryClientProvider client={queryClient}>
        <Story />
      </QueryClientProvider>
    ),
  ],
};

export default preview;

Component-Level Decorators

Apply to all stories in a file:

const meta = {
  component: AdminPanel,
  decorators: [
    (Story) => (
      <MockAuthProvider {...AuthStates.admin}>
        <Story />
      </MockAuthProvider>
    ),
  ],
} satisfies Meta<typeof AdminPanel>;

Story-Level Decorators

Apply to individual stories:

export const WithError: Story = {
  decorators: [
    (Story) => (
      <div data-testid="error-boundary">
        <Story />
      </div>
    ),
  ],
};

Decorator Priority

Decorators are applied in this order (inside to outside):

  1. Global decorators (preview.tsx)
  2. Component-level decorators (meta)
  3. Story-level decorators (individual story)
// Applied order: Story → Component → Global

Best Practices

1. Extract Reusable Decorators

// ✅ Good - Reusable decorator
// tests/mocks/decorators/withAuth.ts
export const withAuth: Decorator = (Story) => (
  <MockAuthProvider {...AuthStates.authenticated}>
    <Story />
  </MockAuthProvider>
);

// Use in stories
import { withAuth } from '@/tests/mocks/decorators/withAuth';

export const Protected: Story = {
  decorators: [withAuth],
};

2. Use Factory Functions for Configuration

// ✅ Good - Configurable decorator
export const withAuth = (state = AuthStates.authenticated): Decorator => 
  (Story) => (
    <MockAuthProvider {...state}>
      <Story />
    </MockAuthProvider>
  );

// Usage
decorators: [withAuth(AuthStates.admin)]

3. Document Decorator Purpose

/**
 * Wraps story with authenticated user context
 * Provides user: { id: 'user-123', email: 'user@example.com' }
 */
const withAuth: Decorator = (Story) => (
  <MockAuthProvider {...AuthStates.authenticated}>
    <Story />
  </MockAuthProvider>
);

4. Keep Decorators Focused

// ✅ Good - Single responsibility
const withAuth: Decorator = /* auth only */;
const withTheme: Decorator = /* theme only */;
const withRouter: Decorator = /* router only */;

// Use together
decorators: [withAuth, withTheme, withRouter]

// ❌ Avoid - Multiple concerns
const withEverything: Decorator = /* auth + theme + router + ... */;

5. Handle Cleanup

const withQueryClient: Decorator = (Story) => {
  const queryClient = new QueryClient();
  
  // Cleanup after unmount
  useEffect(() => {
    return () => {
      queryClient.clear();
    };
  }, []);
  
  return (
    <QueryClientProvider client={queryClient}>
      <Story />
    </QueryClientProvider>
  );
};

6. Type Decorators Properly

import type { Decorator } from '@storybook/react';

// ✅ Good - Properly typed
export const withAuth: Decorator = (Story) => (
  <MockAuthProvider {...AuthStates.authenticated}>
    <Story />
  </MockAuthProvider>
);

// ❌ Avoid - Untyped
export const withAuth = (Story) => /* ... */;

7. Use Context for Shared State

const withSharedState: Decorator = (Story) => {
  const [state, setState] = useState();
  
  return (
    <SharedContext.Provider value={{ state, setState }}>
      <Story />
    </SharedContext.Provider>
  );
};

Common Patterns

Authentication Variants

// Default authenticated
export const Default: Story = {
  decorators: [withAuth],
};

// Admin user
export const AsAdmin: Story = {
  decorators: [withAuth(AuthStates.admin)],
};

// Unauthenticated
export const Unauthenticated: Story = {
  decorators: [withAuth(AuthStates.unauthenticated)],
};

Dark Mode Toggle

export const LightMode: Story = {
  decorators: [
    (Story) => (
      <ThemeProvider defaultTheme="light">
        <Story />
      </ThemeProvider>
    ),
  ],
};

export const DarkMode: Story = {
  decorators: [
    (Story) => (
      <ThemeProvider defaultTheme="dark">
        <Story />
      </ThemeProvider>
    ),
  ],
};

With Mock Data Context

const withMockData: Decorator = (Story) => (
  <MockDataContext.Provider
    value={{
      users: mockUsers,
      organizations: mockOrganizations,
    }}
  >
    <Story />
  </MockDataContext.Provider>
);

export const WithData: Story = {
  decorators: [withMockData],
};

Responsive Container

const withMobileView: Decorator = (Story) => (
  <div style={{ maxWidth: '375px', margin: '0 auto' }}>
    <Story />
  </div>
);

export const Mobile: Story = {
  decorators: [withMobileView],
};

Error Boundary

const withErrorBoundary: Decorator = (Story) => (
  <ErrorBoundary fallback={<div>Error occurred</div>}>
    <Story />
  </ErrorBoundary>
);

export const WithErrorHandling: Story = {
  decorators: [withErrorBoundary],
};

Advanced Patterns

Async Decorator

const withAsyncData: Decorator = (Story) => {
  const [data, setData] = useState(null);
  
  useEffect(() => {
    fetchData().then(setData);
  }, []);
  
  if (!data) return <div>Loading...</div>;
  
  return (
    <DataContext.Provider value={data}>
      <Story />
    </DataContext.Provider>
  );
};

Parameterized Decorator

const withCustomStyles = (styles: React.CSSProperties): Decorator =>
  (Story) => (
    <div style={styles}>
      <Story />
    </div>
  );

export const CustomLayout: Story = {
  decorators: [
    withCustomStyles({
      maxWidth: '800px',
      margin: '0 auto',
      padding: '20px',
    }),
  ],
};

Conditional Rendering

const withFeatureFlag = (flag: string): Decorator =>
  (Story) => {
    const isEnabled = checkFeatureFlag(flag);
    
    if (!isEnabled) {
      return <div>Feature disabled</div>;
    }
    
    return <Story />;
  };


For task-specific implementation details, see the Storybook integration task files in docs/storybook-integration/.