@flagify/react
v1.4.0
Published
React SDK for Flagify — hooks and provider for feature flag evaluation.
Downloads
559
Maintainers
Readme
Overview
@flagify/react is the official React SDK for Flagify. Idiomatic hooks and a context provider for feature flag evaluation in React applications.
- Hooks-first --
useFlag,useVariant,useFlagValuefor every use case - Type-safe -- Full TypeScript generics for flag values
- Zero config -- Wrap with
<FlagifyProvider>, use hooks anywhere - Lightweight -- Thin wrapper over
@flagify/node - React 18+ -- Built for modern React
- React Native ready -- Works in React Native and Expo with zero additional setup
Table of contents
- Installation
- Quick start
- Provider
- User context & targeting
- Hooks
- Examples
- Debug logging
- API reference
- Contributing
- License
Installation
# pnpm
pnpm add @flagify/react
# npm
npm install @flagify/react
# yarn
yarn add @flagify/reactPeer dependency: React 18+ is required.
React Native / Expo
@flagify/react is fully compatible with React Native (0.64+) and Expo (SDK 44+). No separate package or polyfills needed.
npx expo install @flagify/reactWrap your root with <FlagifyProvider> and use hooks anywhere. For a full getting-started guide, see the React Native documentation.
Quick start
1. Wrap your app with the provider
import { FlagifyProvider } from '@flagify/react'
function App() {
return (
<FlagifyProvider projectKey="proj_xxx" publicKey="pk_xxx">
<YourApp />
</FlagifyProvider>
)
}2. Use hooks in any component
import { useFlag } from '@flagify/react'
function Navbar() {
const showBanner = useFlag('promo-banner')
return (
<nav>
{showBanner && <PromoBanner />}
</nav>
)
}Provider
<FlagifyProvider>
Initializes the Flagify client and provides it to all child components via React context.
<FlagifyProvider
projectKey="proj_xxx"
publicKey="pk_xxx"
options={{
apiUrl: 'https://api.flagify.dev',
staleTimeMs: 300_000,
user: {
id: 'user_123',
email: '[email protected]',
role: 'admin',
geolocation: { country: 'US' },
},
}}
>
{children}
</FlagifyProvider>Props
All props from FlagifyOptions are supported:
| Prop | Type | Required | Description |
|------|------|----------|-------------|
| projectKey | string | Yes | Project identifier from your Flagify workspace |
| publicKey | string | Yes | Client-safe publishable API key (pk_*). Never pass a secret key (sk_*) here — secret keys are server-only and belong in @flagify/node, @flagify/nestjs, or the @flagify/astro middleware. The Provider logs a console.error in the browser if it detects a secret-key leak. |
| options | object | No | Additional configuration (apiUrl, staleTimeMs, user, realtime) |
| children | ReactNode | Yes | Your application tree |
Context value
The provider exposes the following context:
| Property | Type | Description |
|----------|------|-------------|
| client | Flagify \| null | The underlying Flagify client instance |
| isReady | boolean | true once the client has been initialized |
<FlagifyAuthProvider>
A thin wrapper around <FlagifyProvider> for the common case where your user lives in another React provider further up the tree — 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 and forwards it.
Use <FlagifyAuthProvider> when <FlagifyProvider> needs to sit below another provider (e.g. ReactQueryProvider) that owns the user. Use <FlagifyProvider> directly when you already have the user synchronously (static, localStorage, Zustand selector).
import { FlagifyAuthProvider } from '@flagify/react'
import { useUserProfileService } from './auth'
function Root() {
return (
<ReactQueryProvider>
<FlagifyAuthProvider
projectKey="proj_xxx"
publicKey="pk_xxx"
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, so it composes with any hook-based source of truth. - Forwards the returned user to
<FlagifyProvider>viaoptions.user. - Computes a
keyfrom the current user and passes it to<FlagifyProvider>so any attribute change — login, logout, impersonation, in-session role or plan upgrade — forces a clean resync. The default strategy hashes the user object with sorted top-level keys for authenticated users (spurious-remount-safe under re-ordered field construction) and returns the literal'anonymous'when the hook returnsnull/undefined, so both directions of the anonymous ↔ authenticated transition remount cleanly.
Return null or undefined from useUserHook for anonymous visitors — the wrapper forwards undefined and keys by 'anonymous'.
Props
| 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 (sorted-key) hash of the user object for authenticated users, or 'anonymous' when the hook returns null/undefined. Supply a narrower function — e.g. (u) => u?.id ?? 'anonymous' — if you want to resync only on id changes. Your override receives null/undefined for anonymous — handle it explicitly. |
| projectKey | string | Yes | Project identifier |
| publicKey | string | Yes | Client-safe publishable API key (pk_*). Secret keys (sk_*) are server-only and not accepted here — use @flagify/node, @flagify/nestjs, or the @flagify/astro middleware for server-side evaluation. |
| options | object | No | Client options (apiUrl, realtime, staleTimeMs, pollIntervalMs, …) except user, which the wrapper owns |
| 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'. The targeting rules themselves are configured server-side in the Flagify dashboard or API. The React SDK only forwards the user attributes.
Since v1.1.0, the Provider always asks the targeting engine on init — even when
options.userisundefined. Catch-all rules and rollout rules that don't depend on user identity apply to anonymous visitors, souseFlag('promo-banner')reflects the rule result from the very first render (afterisReady), not the rawdefaultValue. You only need to passoptions.userwhen you have rules that actually discriminate by user attributes.
The pattern is one-shot, not per-flag:
- After the user is loaded by your auth layer, 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 and re-renders when it changes via SSE.
There is no second hook, no useFlag(key, user) overload, and no need to call client.evaluate() from a component. Do not wrap useFlag in a custom hook that calls client.evaluate(key, user) per flag — it bypasses the cache, is async, and produces a flash of the wrong value.
import { FlagifyProvider, useFlag } from '@flagify/react'
import { useCurrentUser } from './auth'
function Root() {
const user = useCurrentUser() // your app's auth state
return (
<FlagifyProvider
// key forces a fresh client + resync when the user changes (login/logout)
key={user?.id ?? 'anonymous'}
projectKey="proj_xxx"
publicKey="pk_xxx"
options={{
realtime: true,
user: user
? { id: user.id, role: user.role, email: user.email }
: undefined,
}}
>
<App />
</FlagifyProvider>
)
}
function AdminMenu() {
// Already evaluated against targeting rules for the current user.
// 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
<FlagifyProvider> must be 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 / 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).
The simplest pattern is a thin wrapper that reads the user from your auth context and forwards it to <FlagifyProvider>:
import { FlagifyProvider } from '@flagify/react'
import { useCurrentUser } from './auth'
function AppFlagifyProvider({ children }: { children: React.ReactNode }) {
const user = useCurrentUser()
return (
<FlagifyProvider
key={user?.id ?? 'anonymous'}
projectKey="proj_xxx"
publicKey="pk_xxx"
options={{
realtime: true,
user: user
? { id: user.id, role: user.role, email: user.email }
: undefined,
}}
>
{children}
</FlagifyProvider>
)
}
function Root() {
return (
<AuthProvider>
<AppFlagifyProvider>
<ReactQueryProvider>
<Router>{/* the rest of your app */}</Router>
</ReactQueryProvider>
</AppFlagifyProvider>
</AuthProvider>
)
}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 // any custom attribute (plan, companySize, betaCohort, etc.)
}The field is id, not userId. The SDK serializes it to userId on the wire automatically.
When the user changes
The Provider re-syncs flags when options.user.id changes. The simplest way to make this fully reliable across all user attributes (and to invalidate any other client state tied to the previous identity) is to remount the Provider with key={user.id ?? 'anonymous'}. Switching from 'anonymous' to a real id, or between two real ids, will tear down the old client and create a fresh one with the new user, refetching evaluated flags.
For server-side per-request evaluation (e.g. inside Next.js API routes or Express handlers), use flagify.evaluate(key, user) from @flagify/node directly — see the @flagify/node README.
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, no hooks involved
<FlagifyProvider
projectKey="proj_xxx"
publicKey="pk_xxx"
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="proj_xxx"
publicKey="pk_xxx"
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="proj_xxx"
publicKey="pk_xxx"
useUserHook={() => useAuthStore((s) => s.user)}
>
<App />
</FlagifyAuthProvider>4. A sibling provider needs a flag. The trap: <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. Don't invert the tree. Extract the flag consumer into a leaf component and mount it inside the Flagify tree.
// Wrong: putting the useFlag call in ReactQueryProvider itself causes a
// chicken-and-egg because FlagifyProvider needs the user from React Query.
// Right: the gate is a leaf, and it lives inside FlagifyProvider.
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 that comes up on every integration: 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.
The correct pattern is to pass user once to <FlagifyProvider> (or let <FlagifyAuthProvider> do it) and let the SDK evaluate everything against that user locally. Hooks then read from the cache synchronously and re-render via SSE when flags change.
For server-side code that legitimately needs per-request user context (e.g. Next.js API routes), use flagify.evaluate(key, user) from @flagify/node — it's the right tool in a different environment.
Hooks
useFlag(flagKey: string): boolean | undefined
Evaluates a boolean feature flag. Returns undefined while the client is still syncing (isReady === false), then true/false once the cache is populated. Returns false for missing or disabled flags.
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.
function Dashboard() {
const isNew = useFlag('new-dashboard')
// Wait for sync before deciding — avoids a flash of the legacy dashboard.
if (isNew === undefined) return <Spinner />
return isNew ? <NewDashboard /> : <LegacyDashboard />
}useVariant(flagKey: string): string | undefined
Returns the string variant of a multivariate flag. Ideal for A/B tests and experiments.
function Onboarding() {
const variant = useVariant('onboarding-flow')
switch (variant) {
case 'control': return <OnboardingClassic />
case 'variant-a': return <OnboardingShort />
case 'variant-b': return <OnboardingGuided />
default: return <OnboardingClassic />
}
}useFlagValue<T>(flagKey: string): T | undefined
Returns a typed flag value with full TypeScript generics. Supports number, string, boolean, and JSON values.
interface ListConfig {
maxItems: number
showPagination: boolean
}
function ItemList() {
const config = useFlagValue<ListConfig>('list-config')
return (
<ul>
{items.slice(0, config?.maxItems ?? 10).map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
)
}useIsReady(): boolean
Returns true once the Flagify client has completed its initial flag sync. Useful for showing loading states.
function App() {
const isReady = useIsReady()
if (!isReady) return <Spinner />
return <Dashboard />
}useFlagifyClient(): Flagify
Direct access to the underlying Flagify client instance. Throws if used outside of <FlagifyProvider>.
function FeatureGate({ flagKey, children }: { flagKey: string; children: ReactNode }) {
const client = useFlagifyClient()
if (!client.isEnabled(flagKey)) return null
return <>{children}</>
}Examples
Feature gate component
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}</>
}
// Usage
<FeatureGate flag="premium-features" fallback={<UpgradePrompt />}>
<PremiumDashboard />
</FeatureGate>A/B test with analytics
import { useVariant } from '@flagify/react'
import { useEffect } from 'react'
function PricingPage() {
const variant = useVariant('pricing-layout')
useEffect(() => {
analytics.track('pricing_viewed', { variant })
}, [variant])
return variant === 'variant-a'
? <PricingCards />
: <PricingTable />
}Remote config
import { useFlagValue } from '@flagify/react'
interface ThemeConfig {
primaryColor: string
borderRadius: number
fontFamily: string
}
function ThemeProvider({ children }: { children: ReactNode }) {
const theme = useFlagValue<ThemeConfig>('theme-config')
const style = {
'--primary': theme?.primaryColor ?? '#0D80F9',
'--radius': `${theme?.borderRadius ?? 8}px`,
'--font': theme?.fontFamily ?? 'Inter',
} as React.CSSProperties
return <div style={style}>{children}</div>
}Debug logging
@flagify/react is silent in normal operation. To diagnose realtime/SSE issues, opt in via one of two paths:
Browser (recommended, works everywhere). Open DevTools and run:
localStorage.setItem("FLAGIFY_DEBUG", "1");
location.reload();The flag is read once at module load, so the page must reload after toggling it.
Server-side rendering / API routes. Set the env var on the Node process running your SSR or API code:
FLAGIFY_DEBUG=1 next devMost 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), and Metro/Expo require the EXPO_PUBLIC_ prefix. Use localStorage for client-side debugging — the SDK does not look for prefixed copies of the var.
You'll see entries like [Flagify] Realtime connected, [Flagify] Synced N flags via SSE, and [Flagify] Flag changed: <key>. Real errors (missing <FlagifyProvider>, failed evaluation after sync, duplicate connect()) always log regardless.
API reference
| Export | Type | 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 usage) |
| 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 |
| FlagifyProviderProps | Type | Props for FlagifyProvider |
| FlagifyAuthProviderProps | Type | Props for FlagifyAuthProvider |
| FlagifyContextValue | Type | Shape of the context value |
Types re-exported from @flagify/node:
| Export | Description |
|--------|-------------|
| FlagifyOptions | Client configuration |
| FlagifyUser | User context for targeting |
| FlagifyFlag | Flag data structure |
| IFlagifyClient | Client interface |
Contributing
We welcome contributions. Please open an issue first to discuss what you'd like to change.
# Clone
git clone https://github.com/flagifyhq/javascript.git
cd javascript
# Install
pnpm install
# Development (watch mode)
pnpm run dev
# Build
pnpm run buildLicense
MIT -- see LICENSE for details.
