npm package: @supashiphq/react-sdk
Description: Supaship SDK for React
Author: Supaship
Latest version: 1.1.0

# Supaship React SDK

[Supaship](https://supashp.com) SDK for React, that provides hooks and components for feature flag management with full TypeScript type safety.

## Installation

```bash
npm install @supashiphq/react-sdk
# or
yarn add @supashiphq/react-sdk
# or
pnpm add @supashiphq/react-sdk
```

## Quick Start

```tsx
import { SupashipProvider, useFeature, FeaturesWithFallbacks } from '@supashiphq/react-sdk'

// Define your features with type safety
const features = {
  'new-header': false,
  'theme-config': { mode: 'dark' as const, showLogo: true },
  'beta-features': [] as string[],
} satisfies FeaturesWithFallbacks

function App() {
  return (
    <SupashipProvider
      config={{
        sdkKey: 'your-sdk-key',
        environment: 'production',
        features,
        context: {
          userId: '123',
          email: 'user@example.com',
        },
      }}
    >
      <YourApp />
    </SupashipProvider>
  )
}

function YourApp() {
  // Hook returns { feature, isLoading, error, ... }
  const { feature: newHeader, isLoading } = useFeature('new-header')

  if (isLoading) return <div>Loading...</div>

  return <div>{newHeader ? <NewHeader /> : <OldHeader />}</div>
}
```

## Type-Safe Feature Flags

For full TypeScript type safety, define your features and augment the `Features` interface:

```tsx
// lib/features.ts
import { FeaturesWithFallbacks, InferFeatures } from '@supashiphq/react-sdk'

export const FEATURE_FLAGS = {
  'new-header': false,
  'theme-config': {
    mode: 'dark' as const,
    primaryColor: '#007bff',
    showLogo: true,
  },
  'beta-features': [] as string[],
  'disabled-feature': null,
} satisfies FeaturesWithFallbacks

// Type augmentation for global type safety, it is required
declare module '@supashiphq/react-sdk' {
  interface Features extends InferFeatures<typeof FEATURE_FLAGS> {}
}
```

Now `useFeature` and `useFeatures` will have full type safety:

```tsx
function MyComponent() {
  // TypeScript knows 'new-header' is valid and feature is boolean
  const { feature } = useFeature('new-header')

  // TypeScript knows 'theme-config' returns the exact object shape
  const { feature: config } = useFeature('theme-config')
  // config is { mode: 'dark' | 'light', primaryColor: string, showLogo: boolean }

  // TypeScript will error on invalid feature names
  const { feature: invalid } = useFeature('non-existent-feature') // ❌ Type error
}
```

## API Reference

### SupashipProvider

The provider component that makes feature flags available to your React component tree.

```tsx
<SupashipProvider config={config}>{children}</SupashipProvider>
```

**Props:**

| Prop       | Type                   | Required | Description                  |
| ---------- | ---------------------- | -------- | ---------------------------- |
| `config`   | `SupashipClientConfig` | Yes      | Configuration for the client |
| `children` | `React.ReactNode`      | Yes      | Child components             |
| `plugins`  | `SupashipPlugin[]`     | No       | Custom plugins               |
| `toolbar`  | `ToolbarConfig`        | No       | Development toolbar settings |

**Configuration Options:**

```tsx
import type { FeaturesWithFallbacks } from '@supashiphq/react-sdk'

const FEATURE_FLAGS = {
  'my-feature': false,
  config: { theme: 'light' as const },
} satisfies FeaturesWithFallbacks

const config = {
  sdkKey: 'your-sdk-key',
  environment: 'production',
  features: FEATURE_FLAGS,
  context: {
    // Optional: targeting context
    userId: 'user-123',
    email: 'user@example.com',
    plan: 'premium',
  },
  // Hash sensitive context properties such as PII on the client before sending to Edge
  sensitiveContextProperties: ['email', 'userID'],
  networkConfig: {
    // Optional: network settings
    featuresAPIUrl: 'https://api.supashiphq.com/features',
    retry: {
      enabled: true,
      maxAttempts: 3,
      backoff: 1000,
    },
    requestTimeoutMs: 5000,
  },
}
```

> Privacy note: set `sensitiveContextProperties` to hash PII/sensitive context property values on the client before requests are sent to the Edge API.

**Supported Feature Value Types:**

| Type      | Example                             | Description               |
| --------- | ----------------------------------- | ------------------------- |
| `boolean` | `false`                             | Simple on/off flags       |
| `object`  | `{ theme: 'dark', showLogo: true }` | Configuration objects     |
| `array`   | `['feature-a', 'feature-b']`        | Lists of values           |
| `null`    | `null`                              | Disabled/unavailable flag |

> **Note:** Strings and numbers are not supported as standalone feature values. Use objects instead: `{ value: 'string' }` or `{ value: 42 }`.

### useFeature Hook

Retrieves a single feature flag value with React state management and full TypeScript type safety.

```tsx
const result = useFeature(featureName, options?)
```

**Parameters:**

- `featureName: string` - The feature flag key
- `options?: object`
  - `context?: Record<string, unknown>` - Context override for this request
  - `shouldFetch?: boolean` - Whether to fetch the feature (default: true)

**Return Value:**

```tsx
{
  feature: T,               // The feature value (typed based on your Features interface)
  isLoading: boolean,       // Loading state
  isSuccess: boolean,       // Success state
  isError: boolean,         // Error state
  error: Error | null,      // Error object if failed
  status: 'idle' | 'loading' | 'success' | 'error',
  refetch: () => void,      // Function to manually refetch
  // ... other query state properties
}
```

**Examples:**

```tsx
function MyComponent() {
  // Simple boolean feature
  const { feature: isEnabled, isLoading } = useFeature('new-ui')

  if (isLoading) return <Skeleton />

  return <div>{isEnabled ? <NewUI /> : <OldUI />}</div>
}

function ConfigComponent() {
  // Object feature
  const { feature: config } = useFeature('theme-config')

  if (!config) return null

  return (
    <div className={config.theme}>
      {config.showLogo && <Logo />}
      <div style={{ color: config.primaryColor }}>Content</div>
    </div>
  )
}

function ConditionalFetch() {
  const { user, isLoading: userLoading } = useUser()

  // Only fetch when user is loaded
  const { feature } = useFeature('user-specific-feature', {
    context: { userId: user?.id },
    shouldFetch: !userLoading && !!user,
  })

  return <div>{feature && <SpecialContent />}</div>
}
```

### useFeatures Hook

Retrieves multiple feature flags in a single request with type safety.

```tsx
const result = useFeatures(featureNames, options?)
```

**Parameters:**

- `featureNames: readonly string[]` - Array of feature flag keys
- `options?: object`
  - `context?: Record<string, unknown>` - Context override for this request
  - `shouldFetch?: boolean` - Whether to fetch features (default: true)

**Return Value:**

```tsx
{
  features: { [key: string]: T },  // Object with feature values (typed based on keys)
  isLoading: boolean,
  isSuccess: boolean,
  isError: boolean,
  error: Error | null,
  status: 'idle' | 'loading' | 'success' | 'error',
  refetch: () => void,
  // ... other query state properties
}
```

**Examples:**

```tsx
function Dashboard() {
  const { user } = useUser()

  // Fetch multiple features at once (more efficient than multiple useFeature calls)
  const { features, isLoading } = useFeatures(['new-dashboard', 'beta-mode', 'show-sidebar'], {
    context: {
      userId: user?.id,
      plan: user?.plan,
    },
  })

  if (isLoading) return <LoadingSpinner />

  return (
    <div className={features['new-dashboard'] ? 'new-layout' : 'old-layout'}>
      {features['show-sidebar'] && <Sidebar />}
      {features['beta-mode'] && <BetaBadge />}
      <MainContent />
    </div>
  )
}

function FeatureList() {
  // TypeScript will infer the correct types for each feature
  const { features } = useFeatures(['feature-a', 'feature-b', 'config-feature'])

  return (
    <div>
      {features['feature-a'] && <FeatureA />}
      {features['feature-b'] && <FeatureB />}
      {features['config-feature'] && <ConfigDisplay config={features['config-feature']} />}
    </div>
  )
}
```

### SupashipFeature Component

A declarative component for rendering different UI based on boolean feature flag values.

```tsx
<SupashipFeature
  feature="feature-name"
  loading={<Skeleton />}
  variations={{
    true: <ComponentA />,
    false: <ComponentB />,
  }}
/>
```

**Props:**

| Prop          | Type                                    | Required | Description                                                      |
| ------------- | --------------------------------------- | -------- | ---------------------------------------------------------------- |
| `feature`     | `string`                                | Yes      | The feature flag key to evaluate                                 |
| `variations`  | `{ true: ReactNode, false: ReactNode }` | Yes      | Components to render for true/false values                       |
| `loading`     | `ReactNode`                             | No       | Component to render while loading                                |
| `fallback`    | `ReactNode`                             | No       | Component to render when feature value is neither true nor false |
| `context`     | `Record<string, unknown>`               | No       | Context override for this feature                                |
| `shouldFetch` | `boolean`                               | No       | Whether to fetch the feature (default: true)                     |

**Examples:**

```tsx
// Simple boolean feature flag
function Header() {
  return (
    <SupashipFeature
      feature="new-header"
      loading={<HeaderSkeleton />}
      variations={{
        true: <NewHeader />,
        false: <OldHeader />,
      }}
    />
  )
}

// With context
function UserDashboard() {
  const { user } = useAuth()

  return (
    <SupashipFeature
      feature="beta-dashboard"
      context={{ userId: user.id, plan: user.plan }}
      variations={{
        true: <BetaDashboard />,
        false: <StandardDashboard />,
      }}
    />
  )
}
```

### useFeatureContext Hook

Access and update the feature context within components.

```tsx
const { context, updateContext } = useFeatureContext()
```

**Example:**

```tsx
function UserProfileSettings() {
  const { context, updateContext } = useFeatureContext()
  const [user, setUser] = useState(null)

  const handleUserUpdate = newUser => {
    setUser(newUser)

    // Update feature context when user changes
    // This will trigger refetch of all features
    updateContext({
      userId: newUser.id,
      plan: newUser.subscriptionPlan,
      segment: newUser.segment,
    })
  }

  return <form onSubmit={handleUserUpdate}>{/* User profile form */}</form>
}
```

### useClient Hook

Access the underlying SupashipClient instance for advanced use cases.

```tsx
const client = useClient()

// Use client methods directly
const feature = await client.getFeature('my-feature', { context: { ... } })
const features = await client.getFeatures(['feature-1', 'feature-2'])
```

## Best Practices

### 1. Always Use `satisfies` for Feature Definitions

```tsx
// ✅ Good - preserves literal types
const features = {
  'dark-mode': false,
  theme: { mode: 'light' as const, variant: 'compact' as const },
} satisfies FeaturesWithFallbacks

// ❌ Bad - loses literal types (don't use type annotation)
const features: FeaturesWithFallbacks = {
  'dark-mode': false,
  theme: { mode: 'light', variant: 'compact' }, // Types widened to string
}
```

### 2. Centralize Feature Definitions

```tsx
// ✅ Good - centralized feature definitions
// lib/features.ts
export const FEATURE_FLAGS = {
  'new-header': false,
  theme: { mode: 'light' as const },
  'beta-features': [] as string[],
} satisfies FeaturesWithFallbacks

// ❌ Bad - scattered feature definitions
const config1 = { features: { 'feature-1': false } satisfies FeaturesWithFallbacks }
const config2 = { features: { 'feature-2': true } satisfies FeaturesWithFallbacks }
```

### 3. Use Type Augmentation for Type Safety

```tsx
// ✅ Good - type augmentation for global type safety
declare module '@supashiphq/react-sdk' {
  interface Features extends InferFeatures<typeof FEATURE_FLAGS> {}
}

// Now all useFeature calls are type-safe
const { feature } = useFeature('new-header') // ✅ TypeScript knows this is boolean
const { feature } = useFeature('invalid') // ❌ TypeScript error
```

### 3. Use Context for User Targeting

```tsx
function App() {
  const { user } = useAuth()

  return (
    <SupashipProvider
      config={{
        sdkKey: 'your-sdk-key',
        features: FEATURE_FLAGS,
        context: {
          userId: user?.id,
          email: user?.email,
          plan: user?.subscriptionPlan,
          version: process.env.REACT_APP_VERSION,
        },
      }}
    >
      <YourApp />
    </SupashipProvider>
  )
}
```

### 4. Batch Feature Requests

```tsx
// ✅ Good - single API call
const { features } = useFeatures(['feature-1', 'feature-2', 'feature-3'])

// ❌ Less efficient - multiple API calls
const feature1 = useFeature('feature-1')
const feature2 = useFeature('feature-2')
const feature3 = useFeature('feature-3')
```

### 5. Handle Loading States

```tsx
function MyComponent() {
  const { user, isLoading: userLoading } = useUser()

  const { features, isLoading: featuresLoading } = useFeatures(['user-specific-feature'], {
    context: { userId: user?.id },
    shouldFetch: !userLoading && !!user,
  })

  if (userLoading || featuresLoading) return <Skeleton />

  return <div>{features['user-specific-feature'] && <SpecialContent />}</div>
}
```

### 6. Update Context Reactively

```tsx
function UserDashboard() {
  const { updateContext } = useFeatureContext()
  const [currentPage, setCurrentPage] = useState('dashboard')

  // Update context when navigation changes
  useEffect(() => {
    updateContext({ currentPage })
  }, [currentPage, updateContext])

  return (
    <div>
      <Navigation onPageChange={setCurrentPage} />
      <PageContent page={currentPage} />
    </div>
  )
}
```

## Framework Integration

### Next.js App Router (Next.js 13+)

```tsx
// app/providers.tsx
'use client'
import { SupashipProvider, FeaturesWithFallbacks } from '@supashiphq/react-sdk'

const FEATURE_FLAGS = {
  'new-hero': false,
  theme: { mode: 'light' as const },
} satisfies FeaturesWithFallbacks

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <SupashipProvider
      config={{
        sdkKey: process.env.NEXT_PUBLIC_SUPASHIP_SDK_KEY!,
        environment: process.env.NODE_ENV!,
        features: FEATURE_FLAGS,
      }}
    >
      {children}
    </SupashipProvider>
  )
}

// app/layout.tsx
import { Providers } from './providers'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  )
}

// app/page.tsx
;('use client')
import { useFeature } from '@supashiphq/react-sdk'

export default function HomePage() {
  const { feature: newHero } = useFeature('new-hero')

  return <main>{newHero ? <NewHeroSection /> : <OldHeroSection />}</main>
}
```

### Next.js Pages Router (Next.js 12 and below)

```tsx
// pages/_app.tsx
import { SupashipProvider, FeaturesWithFallbacks } from '@supashiphq/react-sdk'
import type { AppProps } from 'next/app'

const FEATURE_FLAGS = {
  'new-homepage': false,
} satisfies FeaturesWithFallbacks

export default function App({ Component, pageProps }: AppProps) {
  return (
    <SupashipProvider
      config={{
        sdkKey: process.env.NEXT_PUBLIC_SUPASHIP_SDK_KEY!,
        environment: process.env.NODE_ENV!,
        features: FEATURE_FLAGS,
      }}
    >
      <Component {...pageProps} />
    </SupashipProvider>
  )
}
```

### Vite / Create React App

```tsx
// src/main.tsx or src/index.tsx
import { SupashipProvider, FeaturesWithFallbacks } from '@supashiphq/react-sdk'

const FEATURE_FLAGS = {
  'new-ui': false,
  theme: { mode: 'light' as const },
} satisfies FeaturesWithFallbacks

function App() {
  return (
    <SupashipProvider
      config={{
        sdkKey: import.meta.env.VITE_SUPASHIP_SDK_KEY, // Vite
        // or
        sdkKey: process.env.REACT_APP_SUPASHIP_SDK_KEY, // CRA
        environment: import.meta.env.MODE,
        features: FEATURE_FLAGS,
      }}
    >
      <YourApp />
    </SupashipProvider>
  )
}
```

## Development Toolbar

The SDK includes a development toolbar for testing and debugging feature flags locally.

```tsx
<SupashipProvider
  config={{ ... }}
  toolbar={{
    enabled: 'auto', // 'auto' | 'always' | 'never'
    position: 'bottom-right', // 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'
  }}
>
  <YourApp />
</SupashipProvider>
```

- `'auto'`: Shows toolbar in development environments only (default)
- `true`: Always shows toolbar
- `false`: Never shows toolbar

The toolbar allows you to:

- View all available feature flags
- Override feature values locally
- See feature value types and current values
- Clear local overrides

## Testing

### Mocking Feature Flags in Tests

```tsx
// test-utils/providers.tsx
import { SupashipProvider, FeaturesWithFallbacks } from '@supashiphq/react-sdk'

export function TestProviders({ children, features = {} as FeaturesWithFallbacks }) {
  return (
    <SupashipProvider
      config={{
        sdkKey: 'test-key',
        environment: 'test',
        features,
        context: {},
      }}
    >
      {children}
    </SupashipProvider>
  )
}
```

### Example Test

```tsx
// MyComponent.test.tsx
import { render, screen } from '@testing-library/react'
import { TestProviders } from '../test-utils/providers'
import MyComponent from './MyComponent'

describe('MyComponent', () => {
  it('shows new feature when enabled', () => {
    render(
      <TestProviders features={{ 'new-feature': true }}>
        <MyComponent />
      </TestProviders>
    )

    expect(screen.getByText('New Feature Content')).toBeInTheDocument()
  })

  it('shows old feature when disabled', () => {
    render(
      <TestProviders features={{ 'new-feature': false }}>
        <MyComponent />
      </TestProviders>
    )

    expect(screen.getByText('Old Feature Content')).toBeInTheDocument()
  })
})
```

## Troubleshooting

### Common Issues

#### Type errors with FeaturesWithFallbacks

If you encounter type errors when defining features, ensure you're using the correct pattern:

**Solution:** Always use `satisfies FeaturesWithFallbacks` (not type annotation)

```tsx
// ✅ Good - preserves literal types
const features = {
  'my-feature': false,
  config: { theme: 'dark' as const },
} satisfies FeaturesWithFallbacks

// ❌ Bad - loses literal types
const features: FeaturesWithFallbacks = {
  'my-feature': false,
  config: { theme: 'dark' }, // Widened to string
}
```

#### Provider Not Found Error

```
Error: useFeature must be used within a SupashipProvider
```

**Solution:** Ensure your component is wrapped in a `SupashipProvider`:

```tsx
// ✅ Correct
function App() {
  return (
    <SupashipProvider config={{ ... }}>
      <MyComponent />
    </SupashipProvider>
  )
}

// ❌ Incorrect
function App() {
  return <MyComponent /> // Missing provider
}
```

#### Features Not Loading

- **Check SDK key:** Verify your SDK key is correct
- **Check network:** Open browser dev tools and check network requests
- **Check features config:** Ensure features are defined in the config

#### Type Errors

```
Property 'my-feature' does not exist on type 'Features'
```

**Solution:** Add type augmentation:

```tsx
import { InferFeatures } from '@supashiphq/react-sdk'
import { FEATURE_FLAGS } from './features'

declare module '@supashiphq/react-sdk' {
  interface Features extends InferFeatures<typeof FEATURE_FLAGS> {}
}
```

## License

This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details.