React
React-specific code example with hooks and components for seamless feature flag integration.
Requirements
- React 16.8+ (hooks support)
- Node.js 16+
Installation
npm install @darkfeature/sdk-react
# or
yarn add @darkfeature/sdk-react
# or
pnpm add @darkfeature/sdk-react
Provider Setup
Wrap your app with the DarkFeatureProvider
:
import React from 'react'
import { DarkFeatureProvider } from '@darkfeature/sdk-react'
import App from './App'
interface AppContext {
userId: string
email: string
version: string
}
function Root() {
const context: AppContext = {
userId: '123',
email: '[email protected]',
version: '1.0.0',
}
return (
<DarkFeatureProvider
config={{
apiKey: 'your-api-key', // Store in environment variable
context,
}}>
<App />
</DarkFeatureProvider>
)
}
export default Root
Component-Based Approach
DarkFeature Component
Use the declarative DarkFeature
component for simple feature flag rendering:
import React from 'react'
import { DarkFeature } from '@darkfeature/sdk-react'
function YourComponent() {
return (
<div>
{/* Declarative component approach */}
<DarkFeature
feature="new-navigation"
fallback={false}
variations={{
true: <NewNavigation />,
false: <OldNavigation />,
}}
/>
</div>
)
}
const NewNavigation: React.FC = () => (
<div>🚀 New Navigation Enabled!</div>
)
const OldNavigation: React.FC = () => (
<div>📦 Classic Navigation</div>
)
Advanced Component Usage
// With context override
<DarkFeature
feature="premium-features"
fallback={false}
context={{ segment: 'premium' }}
variations={{
true: <PremiumFeatures />,
false: <BasicFeatures />,
}}
/>
// With custom fallback component
<DarkFeature
feature="beta-feature"
fallback={<ComingSoon />}
variations={{
true: <BetaFeature />,
false: <ComingSoon />,
}}
/>
Hook-Based Approach
useFeature Hook
Get a single feature flag value with the useFeature
hook:
import React from 'react'
import { useFeature } from '@darkfeature/sdk-react'
function MyComponent() {
// Single feature with hook
const { feature: isEnabled } = useFeature('new-navigation', {
fallback: false,
})
return <div>{isEnabled ? <NewNavigation /> : <OldNavigation />}</div>
}
useFeature with Context Override
// Feature with context override and typing
const { feature: expFeature } =
(useFeature < 'control') |
'variant-a' |
('variant-b' >
('experiment-feature',
{
fallback: 'control',
context: { segment: 'premium' },
}))
return (
<div>
{expFeature === 'variant-a' && <VariantA />}
{expFeature === 'variant-b' && <VariantB />}
{expFeature === 'control' && <Control />}
</div>
)
useFeatures Hook
Get multiple feature flags at once:
import React from 'react'
import { useFeatures } from '@darkfeature/sdk-react'
interface FeatureSet {
'feature-1': boolean
'feature-2': string
'feature-3': null
}
function Dashboard() {
// Multiple features at once with typing
const { features } = useFeatures<FeatureSet>({
features: {
'feature-1': false,
'feature-2': 'default-value',
'feature-3': null,
},
context: { page: 'dashboard' },
})
return (
<div>
{features['feature-1'] && <Feature1Component />}
<Feature2Component value={features['feature-2']} />
{features['feature-3'] && <Feature3Component />}
</div>
)
}
Advanced Provider Configuration
import { DarkFeatureProvider } from '@darkfeature/sdk-react'
function App() {
const context = {
userId: '123',
email: '[email protected]',
version: '1.0.0',
}
return (
<DarkFeatureProvider
config={{
apiKey: process.env.REACT_APP_DARKFEATURE_API_KEY,
context,
baseUrl: 'https://api.darkfeature.com',
timeout: 5000,
cache: {
enabled: true,
ttl: 300000, // 5 minutes
maxSize: 1000,
},
debug: process.env.NODE_ENV === 'development',
}}>
<YourApp />
</DarkFeatureProvider>
)
}
TypeScript Support
import React from 'react'
import { useFeature, DarkFeature } from '@darkfeature/sdk-react'
interface UserContext {
userId: string
email: string
plan?: 'free' | 'premium' | 'enterprise'
}
interface MyComponentProps {
userId: string
}
const MyComponent: React.FC<MyComponentProps> = ({ userId }) => {
// Single feature with proper typing
const { feature: isEnabled } = useFeature('premium-features', {
fallback: false,
})
// Feature with custom type
const { feature: theme } = useFeature<'light' | 'dark'>('theme', {
fallback: 'light',
})
return (
<div className={`theme-${theme}`}>
<DarkFeature
feature='premium-features'
fallback={false}
variations={{
true: <PremiumFeatures userId={userId} />,
false: <BasicFeatures />,
}}
/>
</div>
)
}
Context Management
Updating Context
import React, { useCallback } from 'react'
import { useDarkFeatureContext } from '@darkfeature/sdk-react'
function UserProfile() {
const { updateContext, context } = useDarkFeatureContext()
const handleUpgrade = useCallback(() => {
updateContext({
...context,
plan: 'premium',
upgradeDate: new Date().toISOString(),
})
}, [updateContext, context])
return (
<div>
<h2>Current Plan: {context.plan || 'free'}</h2>
<button onClick={handleUpgrade}>Upgrade to Premium</button>
</div>
)
}
Getting Current Context
import { useDarkFeatureContext } from '@darkfeature/sdk-react'
function ContextDisplay() {
const { context } = useDarkFeatureContext()
return (
<div>
<p>User ID: {context.userId}</p>
<p>Email: {context.email}</p>
<p>Version: {context.version}</p>
</div>
)
}
Error Handling
import React from 'react'
import { useFeature, DarkFeatureProvider } from '@darkfeature/sdk-react'
function ErrorBoundary({ children }) {
return (
<DarkFeatureProvider
config={{
apiKey: 'your-api-key',
context: { userId: '123' },
}}
onError={error => {
console.error('DarkFeature error:', error)
// Send to error tracking service
}}
fallbackComponent={
<div>
Something went wrong with feature flags. Using default behavior.
</div>
}>
{children}
</DarkFeatureProvider>
)
}
function ComponentWithErrorHandling() {
const { feature: isEnabled, error } = useFeature('new-feature', {
fallback: false,
})
// Handle errors at component level
if (error) {
return <div>Using default features due to error</div>
}
return <div>{isEnabled && <NewFeature />}</div>
}
Server-Side Rendering (Next.js)
// pages/_app.js
import { DarkFeatureProvider } from '@darkfeature/sdk-react'
function MyApp({ Component, pageProps, featureFlags }) {
const context = {
userId: pageProps.user?.id || 'anonymous',
email: pageProps.user?.email || '',
version: '1.0.0',
}
return (
<DarkFeatureProvider
config={{
apiKey: process.env.NEXT_PUBLIC_DARKFEATURE_API_KEY,
context,
}}
initialFlags={featureFlags}>
<Component {...pageProps} />
</DarkFeatureProvider>
)
}
// pages/index.js
export async function getServerSideProps(context) {
const user = getUserFromContext(context)
// Pre-fetch feature flags on server
const featureFlags = await fetchFeatureFlags(user)
return {
props: {
user,
featureFlags,
},
}
}
Testing
import React from 'react'
import { render } from '@testing-library/react'
import { DarkFeatureProvider } from '@darkfeature/sdk-react'
import MyComponent from './MyComponent'
// Mock provider for testing
function TestWrapper({ children, mockFlags = {} }) {
const context = {
userId: 'test-user',
email: '[email protected]',
version: '1.0.0',
}
return (
<DarkFeatureProvider
config={{
apiKey: 'test-key',
context,
}}
mockFlags={mockFlags}>
{children}
</DarkFeatureProvider>
)
}
test('shows new feature when enabled', () => {
const { getByText } = render(
<TestWrapper mockFlags={{ 'new-feature': true }}>
<MyComponent />
</TestWrapper>,
)
expect(getByText('New Feature')).toBeInTheDocument()
})
test('uses fallback when feature is disabled', () => {
const { getByText } = render(
<TestWrapper mockFlags={{ 'new-feature': false }}>
<MyComponent />
</TestWrapper>,
)
expect(getByText('Old Feature')).toBeInTheDocument()
})
Performance Optimization
import React, { memo } from 'react'
import { useFeature } from '@darkfeature/sdk-react'
// Memoize components that depend on feature flags
const ExpensiveComponent = memo(() => {
const { feature: isEnabled } = useFeature('expensive-feature', {
fallback: false,
})
if (!isEnabled) {
return null
}
return <VeryExpensiveFeature />
})
// Use feature flags in useMemo/useCallback dependencies
function OptimizedComponent() {
const { feature: isFeatureAEnabled } = useFeature('feature-a', {
fallback: false,
})
const memoizedValue = useMemo(() => {
return isFeatureAEnabled ? 'A' : 'B'
}, [isFeatureAEnabled])
return <div>{memoizedValue}</div>
}
// Batch feature evaluations
function BatchComponent() {
const { features } = useFeatures({
features: {
'feature-a': false,
'feature-b': false,
'feature-c': false,
},
})
return (
<div>
{features['feature-a'] && <FeatureA />}
{features['feature-b'] && <FeatureB />}
{features['feature-c'] && <FeatureC />}
</div>
)
}
Best Practices
1. Provider Placement
Place the provider as high as possible in your component tree:
// ✅ Good - Provider at root level
function App() {
return (
<DarkFeatureProvider config={{ apiKey: 'key', context: {} }}>
<Router>
<Routes />
</Router>
</DarkFeatureProvider>
)
}
// ❌ Avoid - Provider too low in tree
function SomeComponent() {
return (
<div>
<DarkFeatureProvider config={{ apiKey: 'key', context: {} }}>
<ChildComponent />
</DarkFeatureProvider>
</div>
)
}
2. Error Boundaries
Wrap the provider with error boundaries:
import { ErrorBoundary } from 'react-error-boundary'
function App() {
return (
<ErrorBoundary fallback={<ErrorFallback />}>
<DarkFeatureProvider config={{ apiKey: 'key', context: {} }}>
<YourApp />
</DarkFeatureProvider>
</ErrorBoundary>
)
}
3. Loading States
Always handle loading states gracefully:
function MyComponent() {
const { feature: isEnabled, loading } = useFeature('new-feature', {
fallback: false,
})
if (loading) {
return <LoadingSpinner />
}
return isEnabled ? <NewFeature /> : <OldFeature />
}
4. Memoization
Use React.memo for components that depend on feature flags:
const FeatureDependentComponent = memo(({ userId }) => {
const { feature: isEnabled } = useFeature('premium-features', {
fallback: false,
})
return isEnabled ? <PremiumUI userId={userId} /> : <BasicUI userId={userId} />
})
5. TypeScript Usage
Use proper typing for better development experience:
// Define feature types
type FeatureFlags = {
'new-navigation': boolean
theme: 'light' | 'dark'
experiment: 'control' | 'variant-a' | 'variant-b'
}
// Use in components
const { feature: theme } = useFeature<FeatureFlags['theme']>('theme', {
fallback: 'light',
})
Troubleshooting
Common Issues
Provider Not Found
// Make sure DarkFeatureProvider wraps your component
function MyComponent() {
const { feature } = useFeature('feature', { fallback: false }) // ❌ Error
return <div>...</div>
}
// ✅ Correct
function App() {
return (
<DarkFeatureProvider config={{ apiKey: 'key', context: {} }}>
<MyComponent />
</DarkFeatureProvider>
)
}
Context Not Updating
// Use updateContext to change context
const { updateContext, context } = useDarkFeatureContext()
const handleLogin = user => {
updateContext({
...context,
userId: user.id,
email: user.email,
})
}
Feature Always Returns Fallback
// Check if context is properly set
const { context } = useDarkFeatureContext()
console.log('Current context:', context)
// Check feature configuration
const { feature, error } = useFeature('test-feature', { fallback: 'test' })
console.log('Feature value:', feature, 'Error:', error)
Last updated on