React SDK
The @flagify/react package provides React hooks and a context provider for evaluating feature flags in React applications.
Installation
npm install @flagify/react
Setup
Wrap your application with the FlagifyProvider:
import { FlagifyProvider } from '@flagify/react';
function App() {
return (
<FlagifyProvider
projectKey="my-project"
publicKey="pk_dev_abc123_xxxxxxxx"
options={{ realtime: true }}
>
<YourApp />
</FlagifyProvider>
);
}
Provider props
The provider accepts all FlagifyOptions from @flagify/node plus children:
| Prop | Type | Required | Description |
|---|---|---|---|
projectKey | string | Yes | Your project key |
publicKey | string | Yes | Publishable API key (pk_*). Secret keys (sk_*) are not accepted here — they belong in server SDKs (@flagify/node, @flagify/nestjs, or the @flagify/astro middleware). The Provider logs a console.error in the browser if a secret key slips through. |
options.realtime | boolean | No | Enable real-time SSE updates |
options.staleTimeMs | number | No | Cache stale threshold in ms |
options.apiUrl | string | No | Custom API base URL |
options.user | FlagifyUser | No | User context for targeting (see below) |
children | ReactNode | Yes | Your application tree |
User context & targeting
Targeting rules let a flag return different values per user — for example, an admin-tools flag that’s only true for users whose role === 'admin', or a beta-features flag enabled for plan === 'enterprise'. Targeting rules are configured server-side (Flagify dashboard or API); the React SDK only forwards user attributes.
The pattern is one-shot, not per-flag:
- After your auth layer loads the user, mount
<FlagifyProvider>withoptions.user. - The Provider’s underlying client fetches all flag values already evaluated against the targeting rules for that user and stores them in its local cache.
useFlag('admin-tools')reads the cached, already-targeted value.
import { FlagifyProvider, useFlag } from '@flagify/react';
import { useCurrentUser } from './auth';
function Root() {
const user = useCurrentUser();
return (
<FlagifyProvider
key={user?.id ?? 'anonymous'}
projectKey="my-project"
publicKey="pk_dev_abc123_xxxxxxxx"
options={{
realtime: true,
user: user
? { id: user.id, role: user.role, email: user.email }
: undefined,
}}
>
<App />
</FlagifyProvider>
);
}
function AdminMenu() {
// useFlag returns `undefined` until the initial sync completes, so compare
// explicitly — don't assume truthiness.
const canSeeAdmin = useFlag('admin-tools');
if (canSeeAdmin !== true) return null;
return <Admin />;
}
Where to mount the Provider
Mount <FlagifyProvider> below the provider that loads your user, so the user is available when the Flagify client initializes. If the Provider mounts before the user is known, the cache is populated with the anonymous evaluations — catch-all and rollout rules still apply correctly, but any rule that targets by user attributes will miss until the Provider remounts with the real user (use key={user?.id ?? 'anonymous'} to force that resync on login/logout).
<FlagifyAuthProvider>
<FlagifyAuthProvider> is a thin wrapper around <FlagifyProvider> for the common case where your user lives in another React provider — React Query, Zustand, Redux, or any context-based auth layer. Instead of manually wiring options.user and key= on every render, pass a useUserHook prop and the wrapper reads the user from your source-of-truth on each render.
import { FlagifyAuthProvider } from '@flagify/react';
import { useUserProfileService } from './auth';
function Root() {
return (
<ReactQueryProvider>
<FlagifyAuthProvider
projectKey="my-project"
publicKey="pk_dev_abc123_xxxxxxxx"
useUserHook={() => {
const { data } = useUserProfileService();
return data
? { id: data.id, role: data.role, email: data.email }
: null;
}}
options={{ realtime: true }}
>
<App />
</FlagifyAuthProvider>
</ReactQueryProvider>
);
}
The wrapper calls useUserHook() on every render, forwards the returned user to <FlagifyProvider> via options.user, and computes a key from the user that forces a clean resync on any attribute change — login, logout, impersonation, in-session role or plan upgrade, custom-attribute change. The default keying strategy hashes the whole user object for authenticated users and uses the literal string 'anonymous' when the hook returns null/undefined, so the transitions in both directions (anonymous ↔ authenticated) remount cleanly.
| Prop | Type | Required | Description |
|---|---|---|---|
useUserHook | () => FlagifyUser | null | undefined | Yes | React hook called on every render; returns the current user or nullish for anonymous |
userKey | (user: FlagifyUser | null | undefined) => string | No | Override the remount key builder. Defaults to a stable hash of the user object for authenticated users, or 'anonymous' when the hook returns null/undefined. |
projectKey | string | Yes | Project identifier |
publicKey | string | Yes | Client-safe publishable API key (pk_*). Secret keys (sk_*) are not accepted — use server SDKs for server-side evaluation. |
options | object | No | All nested FlagifyOptions.options fields (realtime, apiUrl, staleTimeMs, pollIntervalMs, …) except user, which the wrapper owns |
children | ReactNode | Yes | Your application tree |
Common provider tree patterns
Real apps rarely have a simple <Auth><Flagify><App /></Flagify></Auth> tree. Here are the four patterns we’ve seen and the provider that fits each one.
1. Plain auth (user synchronously available). The user comes from localStorage, a server-rendered cookie, or any source that resolves before React mounts. Use <FlagifyProvider> directly.
const user = readUserFromCookie(); // sync
<FlagifyProvider
projectKey="my-project"
publicKey="pk_dev_abc123_xxxxxxxx"
options={{ user }}
>
<App />
</FlagifyProvider>
2. Auth via React Query. The user is fetched asynchronously with useQuery or similar. <FlagifyProvider> needs to sit below <ReactQueryProvider> — use <FlagifyAuthProvider> so the hook call happens inside the React tree without violating the rules of hooks.
<ReactQueryProvider>
<FlagifyAuthProvider
projectKey="my-project"
publicKey="pk_dev_abc123_xxxxxxxx"
useUserHook={() => {
const { data } = useUserProfileService();
return data ? { id: data.id, role: data.role } : null;
}}
>
<App />
</FlagifyAuthProvider>
</ReactQueryProvider>
3. Auth via Zustand / Redux selector. The user lives in a synchronous store selector. Use <FlagifyAuthProvider> with the selector as the hook.
<FlagifyAuthProvider
projectKey="my-project"
publicKey="pk_dev_abc123_xxxxxxxx"
useUserHook={() => useAuthStore((s) => s.user)}
>
<App />
</FlagifyAuthProvider>
4. A sibling provider needs a flag. <ReactQueryProvider> (or <ThemeProvider>, <I18nProvider>, etc.) wants to gate its own setup on a flag — but <FlagifyProvider> is below it, so it can’t use useFlag from its own scope. The fix is to extract the flag consumer into a leaf component and mount it inside the Flagify tree.
// useUserQueryHook is your app's hook that returns the current user
// from React Query (e.g. `() => useUserProfileService().data ?? null`).
function ReactQueryDevtoolsGate() {
const showDevtools = useFlag('react-query-devtools');
if (showDevtools !== true) return null;
return <ReactQueryDevtools />;
}
<ReactQueryProvider>
<FlagifyAuthProvider useUserHook={useUserQueryHook} projectKey="…" publicKey="…">
<App />
<ReactQueryDevtoolsGate />
</FlagifyAuthProvider>
</ReactQueryProvider>
Why useFlag has no user argument
A question every integrator asks: why not just useFlag('admin-tools', user)? The answer is three constraints that make the synchronous, cache-first API possible:
- It would make
useFlagasynchronous. Passing a new user per call would bypass the cache (or require per-user caches keyed on every render), meaning every call would suspend or return a loading state. You’d be back toif (flag === undefined) return <Spinner />on every feature gate. - It breaks the SSE streaming model. The server streams flag changes to the single user the client was initialized with. An ad-hoc user that only appears in one render has no subscription, so you’d silently miss updates.
- It fans out HTTP. Each
useFlag(key, user)with a new identity is a new evaluation request. Multiply by every flag × every component × every render and the request count explodes — defeating the local cache entirely.
When the user changes (login / logout)
The Provider re-syncs flags when options.user.id changes. The simplest reliable pattern is to remount the Provider with key={user?.id ?? 'anonymous'} — switching from anonymous to a real id, or between two real ids, tears down the old client and creates a fresh one with the new user.
User object shape
{
id: string // required — the user identifier (NOT "userId")
email?: string
role?: string
group?: string
geolocation?: { country?: string; region?: string; city?: string }
[key: string]: unknown // custom attributes (plan, betaCohort, ...)
}
The field is id, not userId. The SDK serializes it to userId on the wire automatically.
See also: targeting concepts and the JavaScript SDK reference for the server-side flagify.evaluate(key, user) pattern introduced above.
Hooks
useFlag
Evaluate a boolean flag. Returns boolean | undefined — undefined while the client is still syncing (isReady === false), then true/false once the cache is populated. When the flag is disabled, returns its offValue. Returns false if the flag doesn’t exist.
Because the first render can be undefined, gate on an explicit comparison (or use useIsReady) instead of relying on truthiness — especially for flags whose “off” state is visible UI.
import { useFlag } from '@flagify/react';
function Navbar() {
const showNewNav = useFlag('new-navbar');
// Wait for sync before deciding — avoids a flash of the classic navbar.
if (showNewNav === undefined) return <NavbarSkeleton />;
return showNewNav ? <NewNavbar /> : <ClassicNavbar />;
}
useVariant
Get the winning variant key for a multivariate flag. Returns the variant with the highest weight:
import { useVariant } from '@flagify/react';
function Checkout() {
const variant = useVariant('checkout-flow', 'classic');
switch (variant) {
case 'streamlined':
return <StreamlinedCheckout />;
case 'classic':
default:
return <ClassicCheckout />;
}
}
useFlagValue
Get a typed value from a flag. When disabled, returns the server’s offValue. The fallback is used only when the flag doesn’t exist in cache:
import { useFlagValue } from '@flagify/react';
function Banner() {
const message = useFlagValue<string>('banner-message', 'Welcome!');
return <div className="banner">{message}</div>;
}
const config = useFlagValue<{ maxItems: number }>('cart-config', { maxItems: 50 });
useIsReady
Check if the initial flag sync is complete:
import { useIsReady, useFlag } from '@flagify/react';
function Feature() {
const isReady = useIsReady();
const enabled = useFlag('new-feature');
if (!isReady) return <Skeleton />;
return enabled ? <NewFeature /> : <OldFeature />;
}
useFlagifyClient
Access the underlying Flagify client directly:
import { useFlagifyClient } from '@flagify/react';
function DebugPanel() {
const client = useFlagifyClient();
// Use client.isEnabled(), client.getValue(), etc.
}
Examples
Feature gate component
A reusable component that renders its children only when a flag is on.
import { useFlag } from '@flagify/react';
import type { ReactNode } from 'react';
function FeatureGate({ flag, children, fallback }: {
flag: string;
children: ReactNode;
fallback?: ReactNode;
}) {
const isEnabled = useFlag(flag);
return <>{isEnabled ? children : fallback}</>;
}
<FeatureGate flag="premium-features" fallback={<UpgradePrompt />}>
<PremiumDashboard />
</FeatureGate>
A/B test with analytics
Track variant exposure the moment a component renders.
import { useVariant } from '@flagify/react';
import { useEffect } from 'react';
function PricingPage() {
const variant = useVariant('pricing-layout', 'classic');
useEffect(() => {
analytics.track('pricing_viewed', { variant });
}, [variant]);
return variant === 'variant-a' ? <PricingCards /> : <PricingTable />;
}
Remote config via typed values
Drive runtime configuration from a JSON flag.
import { useFlagValue } from '@flagify/react';
interface ThemeConfig {
primaryColor: string;
borderRadius: number;
fontFamily: string;
}
function ThemeProvider({ children }: { children: React.ReactNode }) {
const theme = useFlagValue<ThemeConfig>('theme-config', {
primaryColor: '#0D80F9',
borderRadius: 8,
fontFamily: 'Inter',
});
const style = {
'--primary': theme.primaryColor,
'--radius': `${theme.borderRadius}px`,
'--font': theme.fontFamily,
} as React.CSSProperties;
return <div style={style}>{children}</div>;
}
API reference
All public exports from @flagify/react:
| Export | Kind | Description |
|---|---|---|
FlagifyProvider | Component | Context provider — wraps your app |
FlagifyAuthProvider | Component | Wrapper that reads the user from a useUserHook prop and forwards it to FlagifyProvider |
FlagifyContext | React.Context | Raw context (advanced — prefer the hooks below) |
useFlag | Hook | Boolean flag evaluation |
useVariant | Hook | String variant evaluation |
useFlagValue | Hook | Typed value evaluation with generics |
useIsReady | Hook | Client readiness check |
useFlagifyClient | Hook | Direct client access (throws outside FlagifyProvider) |
FlagifyProviderProps | Type | Props for FlagifyProvider |
FlagifyAuthProviderProps | Type | Props for FlagifyAuthProvider |
FlagifyContextValue | Type | Shape of the context value |
Re-exported from @flagify/node for convenience:
| Export | Description |
|---|---|
FlagifyOptions | Client configuration |
FlagifyUser | User context for targeting |
FlagifyFlag | Flag data structure |
IFlagifyClient | Client interface |
Real-time updates
When realtime: true is set on the provider, all hooks automatically re-render when flags change via SSE. No additional configuration needed.
<FlagifyProvider
projectKey="my-project"
publicKey="pk_dev_abc123_xxxxxxxx"
options={{ realtime: true }}
>
{/* Components using useFlag will re-render when flags change */}
<YourApp />
</FlagifyProvider>
Debug logging
The SDK is silent in production. Activate verbose SSE logs via one of two paths:
Browser (recommended). Open DevTools and run:
localStorage.setItem("FLAGIFY_DEBUG", "1");
location.reload();
The flag is read once at module load — reload after toggling.
Server-side (SSR, Next.js server components / API routes). Set the env var on the Node process:
FLAGIFY_DEBUG=1 next dev
Most bundlers do not inline arbitrary process.env.* into client-side code: Next.js requires the NEXT_PUBLIC_ prefix, Vite requires VITE_* (or a define config). The SDK does not look for prefixed copies of the var, so use the localStorage path for client-side debugging.
You’ll see [Flagify] Realtime connected, [Flagify] Synced N flags via SSE, [Flagify] Flag changed: <key>, and idle-timeout warnings. Real errors — missing <FlagifyProvider>, failed evaluation after sync, duplicate connect() — always log regardless.