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:

PropTypeRequiredDescription
projectKeystringYesYour project key
publicKeystringYesPublishable 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.realtimebooleanNoEnable real-time SSE updates
options.staleTimeMsnumberNoCache stale threshold in ms
options.apiUrlstringNoCustom API base URL
options.userFlagifyUserNoUser context for targeting (see below)
childrenReactNodeYesYour 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:

  1. After your auth layer loads the user, mount <FlagifyProvider> with options.user.
  2. 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.
  3. 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.

PropTypeRequiredDescription
useUserHook() => FlagifyUser | null | undefinedYesReact hook called on every render; returns the current user or nullish for anonymous
userKey(user: FlagifyUser | null | undefined) => stringNoOverride the remount key builder. Defaults to a stable hash of the user object for authenticated users, or 'anonymous' when the hook returns null/undefined.
projectKeystringYesProject identifier
publicKeystringYesClient-safe publishable API key (pk_*). Secret keys (sk_*) are not accepted — use server SDKs for server-side evaluation.
optionsobjectNoAll nested FlagifyOptions.options fields (realtime, apiUrl, staleTimeMs, pollIntervalMs, …) except user, which the wrapper owns
childrenReactNodeYesYour 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:

  1. It would make useFlag asynchronous. 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 to if (flag === undefined) return <Spinner /> on every feature gate.
  2. 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.
  3. 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 | undefinedundefined 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:

ExportKindDescription
FlagifyProviderComponentContext provider — wraps your app
FlagifyAuthProviderComponentWrapper that reads the user from a useUserHook prop and forwards it to FlagifyProvider
FlagifyContextReact.ContextRaw context (advanced — prefer the hooks below)
useFlagHookBoolean flag evaluation
useVariantHookString variant evaluation
useFlagValueHookTyped value evaluation with generics
useIsReadyHookClient readiness check
useFlagifyClientHookDirect client access (throws outside FlagifyProvider)
FlagifyProviderPropsTypeProps for FlagifyProvider
FlagifyAuthProviderPropsTypeProps for FlagifyAuthProvider
FlagifyContextValueTypeShape of the context value

Re-exported from @flagify/node for convenience:

ExportDescription
FlagifyOptionsClient configuration
FlagifyUserUser context for targeting
FlagifyFlagFlag data structure
IFlagifyClientClient 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.