Skip to content

React Integration

Featsync works great with React. Here’s how to set up hooks for a clean integration.

Setup

First, create a provider and hooks file:

lib/featsync.tsx
import { useState, useEffect, createContext, useContext, useRef } from 'react';
import { Featsync } from '@featsync/sdk';
// Create a single SDK instance
const featsync = new Featsync({
apiKey: process.env.REACT_APP_FEATSYNC_API_KEY,
cacheMs: 60000,
persist: true,
});
// Context
const FeatsyncContext = createContext(null);
// Provider component
export function FeatsyncProvider({ children }) {
const [flags, setFlags] = useState({});
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const isMounted = useRef(true);
const fetchFlags = async (forceRefresh = false) => {
try {
if (forceRefresh) {
featsync.clearCache();
}
const allFlags = await featsync.getAllFlags();
if (isMounted.current) {
setFlags(allFlags);
setLoading(false);
setError(null);
}
} catch (err) {
if (isMounted.current) {
setError(err);
setLoading(false);
}
}
};
useEffect(() => {
isMounted.current = true;
fetchFlags();
return () => {
isMounted.current = false;
};
}, []);
const refresh = () => fetchFlags(true);
return (
<FeatsyncContext.Provider value={{ flags, loading, error, refresh }}>
{children}
</FeatsyncContext.Provider>
);
}
// Hook: Get all flags
export function useFlags() {
const context = useContext(FeatsyncContext);
if (!context) {
throw new Error('useFlags must be used within a FeatsyncProvider');
}
return context;
}
// Hook: Get a single flag
export function useFlag(key, defaultValue = false) {
const { flags, loading } = useFlags();
if (loading) {
return { enabled: defaultValue, loading: true };
}
return {
enabled: flags[key]?.enabled ?? defaultValue,
percentage: flags[key]?.percentage ?? null,
loading: false,
};
}
// Hook: Check flag for a specific user (percentage rollouts)
export function useFlagForUser(key, userId, defaultValue = false) {
const [enabled, setEnabled] = useState(defaultValue);
const [loading, setLoading] = useState(true);
const { flags } = useFlags();
useEffect(() => {
let mounted = true;
featsync
.isEnabledForUser(key, userId, defaultValue)
.then((result) => {
if (mounted) {
setEnabled(result);
setLoading(false);
}
})
.catch(() => {
if (mounted) {
setEnabled(defaultValue);
setLoading(false);
}
});
return () => {
mounted = false;
};
}, [key, userId, defaultValue]);
return {
enabled,
percentage: flags[key]?.percentage ?? null,
loading,
};
}
// Export SDK instance for direct access
export { featsync };

Wrap Your App

Add the provider to your app’s root:

App.tsx
import { FeatsyncProvider } from './lib/featsync';
function App() {
return (
<FeatsyncProvider>
<YourApp />
</FeatsyncProvider>
);
}

Using the Hooks

Simple Boolean Flag

import { useFlag } from './lib/featsync';
function Navigation() {
const { enabled, loading } = useFlag('new-navigation');
if (loading) {
return <div>Loading...</div>;
}
return enabled ? <NewNavigation /> : <OldNavigation />;
}

Percentage Rollout

import { useFlagForUser } from './lib/featsync';
function Checkout({ userId }) {
const { enabled, loading, percentage } = useFlagForUser('new-checkout', userId);
if (loading) {
return <div>Loading...</div>;
}
return (
<div>
{percentage && <small>Rollout: {percentage}%</small>}
{enabled ? <NewCheckout /> : <OldCheckout />}
</div>
);
}

Check Multiple Flags

import { useFlags } from './lib/featsync';
function Dashboard() {
const { flags, loading, refresh } = useFlags();
if (loading) {
return <div>Loading...</div>;
}
return (
<div>
{flags['show-analytics']?.enabled && <AnalyticsWidget />}
{flags['show-notifications']?.enabled && <NotificationWidget />}
<button onClick={refresh}>Refresh Flags</button>
</div>
);
}

Conditional Rendering Patterns

Show/Hide Components

function FeatureSection() {
const { enabled } = useFlag('new-feature');
if (!enabled) return null;
return <NewFeature />;
}

Toggle Between Implementations

function PaymentForm() {
const { enabled } = useFlag('use-stripe');
return enabled ? <StripePayment /> : <PayPalPayment />;
}

Loading States

function Feature() {
const { enabled, loading } = useFlag('my-feature');
if (loading) {
return <Skeleton />;
}
return enabled ? <NewFeature /> : <OldFeature />;
}

TypeScript

Here’s the TypeScript version of the hooks:

lib/featsync.tsx
import { useState, useEffect, createContext, useContext, useRef, ReactNode } from 'react';
import { Featsync, FlagsMap } from '@featsync/sdk';
interface FeatsyncContextValue {
flags: FlagsMap;
loading: boolean;
error: Error | null;
refresh: () => void;
}
interface FlagValue {
enabled: boolean;
percentage: number | null;
loading: boolean;
}
const FeatsyncContext = createContext<FeatsyncContextValue | null>(null);
export function FeatsyncProvider({ children }: { children: ReactNode }) {
// ... same implementation
}
export function useFlags(): FeatsyncContextValue {
const context = useContext(FeatsyncContext);
if (!context) {
throw new Error('useFlags must be used within a FeatsyncProvider');
}
return context;
}
export function useFlag(key: string, defaultValue = false): FlagValue {
// ... same implementation
}
export function useFlagForUser(key: string, userId: string, defaultValue = false): FlagValue {
// ... same implementation
}

Next.js App Router

For Next.js App Router, make the provider a client component:

providers/featsync.tsx
'use client';
import { FeatsyncProvider } from '@/lib/featsync';
export function Providers({ children }) {
return <FeatsyncProvider>{children}</FeatsyncProvider>;
}
app/layout.tsx
import { Providers } from './providers/featsync';
export default function RootLayout({ children }) {
return (
<html>
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}

Server Components

For Server Components, use the SDK directly:

app/page.tsx
import { Featsync } from '@featsync/sdk';
const featsync = new Featsync({
apiKey: process.env.FEATSYNC_API_KEY,
});
export default async function Page() {
const showBanner = await featsync.isEnabled('show-banner');
return (
<div>
{showBanner && <Banner />}
<Content />
</div>
);
}

Next Steps