npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@flagify/react

v1.4.0

Published

React SDK for Flagify — hooks and provider for feature flag evaluation.

Downloads

559

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, useFlagValue for 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

# pnpm
pnpm add @flagify/react

# npm
npm install @flagify/react

# yarn
yarn add @flagify/react

Peer 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/react

Wrap 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:

  1. Calls useUserHook() on every render, so it composes with any hook-based source of truth.
  2. Forwards the returned user to <FlagifyProvider> via options.user.
  3. Computes a key from 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 returns null/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.user is undefined. Catch-all rules and rollout rules that don't depend on user identity apply to anonymous visitors, so useFlag('promo-banner') reflects the rule result from the very first render (after isReady), not the raw defaultValue. You only need to pass options.user when you have rules that actually discriminate by user attributes.

The pattern is one-shot, not per-flag:

  1. After the user is loaded by your auth layer, 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 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:

  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.

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 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), 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 build

License

MIT -- see LICENSE for details.