@supashiphq/react-sdk
v1.1.0
Published
Supaship SDK for React
Downloads
125
Readme
Supaship React SDK
Supaship SDK for React, that provides hooks and components for feature flag management with full TypeScript type safety.
Installation
npm install @supashiphq/react-sdk
# or
yarn add @supashiphq/react-sdk
# or
pnpm add @supashiphq/react-sdkQuick Start
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: '[email protected]',
},
}}
>
<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:
// 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:
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.
<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:
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: '[email protected]',
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
sensitiveContextPropertiesto 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.
const result = useFeature(featureName, options?)Parameters:
featureName: string- The feature flag keyoptions?: objectcontext?: Record<string, unknown>- Context override for this requestshouldFetch?: boolean- Whether to fetch the feature (default: true)
Return Value:
{
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:
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.
const result = useFeatures(featureNames, options?)Parameters:
featureNames: readonly string[]- Array of feature flag keysoptions?: objectcontext?: Record<string, unknown>- Context override for this requestshouldFetch?: boolean- Whether to fetch features (default: true)
Return Value:
{
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:
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.
<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:
// 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.
const { context, updateContext } = useFeatureContext()Example:
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.
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
// ✅ 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
// ✅ 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
// ✅ 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 error3. Use Context for User Targeting
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
// ✅ 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
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
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+)
// 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)
// 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
// 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.
<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 toolbarfalse: 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
// 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
// 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)
// ✅ 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 SupashipProviderSolution: Ensure your component is wrapped in a SupashipProvider:
// ✅ 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:
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 file for details.
