Flaggy
Get started
← Blog

Using feature flags in React

How to implement feature flags in React — create one shared client, initialize it before render, then call isEnabled directly for conditional rendering, route gating, and per-user targeting.

React needs almost nothing special for feature flags. You create one shared client, initialize it once before you render, then call isEnabled directly wherever you branch. Evaluation is a synchronous in-memory lookup against rules the client already downloaded — there’s no network call per check, so no context provider and no custom hook are needed.

If you’re new to the concept itself, start with what are feature flags and come back. This guide assumes you know what a flag is and want to use one in React.

Set up the client

flaggy() returns a global singleton, so create it once in its own module and import it wherever you need it.

// src/lib/flaggy.ts
import { flaggy } from '@flaggy.io/sdk-js';

export const flagClient = flaggy({
  apiKey: import.meta.env.VITE_FLAGGY_API_KEY!,
  environment: import.meta.env.VITE_ENVIRONMENT,
});

A client-side API key is safe to expose — it only authorizes reading flag rules, not changing them.

Initialize before you render

Initialize once, before rendering, so flags are ready and you avoid a flash of the wrong content.

// src/main.tsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { flagClient } from './lib/flaggy';
import App from './App';

await flagClient.initialize();

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
);

initialize() resolves immediately from a warm localStorage cache when one exists, and otherwise waits for the first fetch. Top-level await is supported by modern bundlers like Vite.

Check a flag

Because the client already has the ruleset in memory, you just call isEnabled directly in any component — no provider, no hook, no loading state.

import { flagClient } from './lib/flaggy';

function Header({ user }: { user: User }) {
  if (flagClient.isEnabled('release-new-header', { key: user.id, plan: user.plan })) {
    return <NewHeader />;
  }
  return <LegacyHeader />;
}

The first argument is the flag key; the optional second is the evaluation context used for targeting (more on that below). That’s the whole pattern — most flag checks in a React app are a single if or ternary like this.

Gate a route

For features that are whole pages, branch at the route. With React Router, a small wrapper redirects when the flag is off — again, just a direct isEnabled call.

import { Navigate } from 'react-router-dom';
import { flagClient } from './lib/flaggy';

function RequireFlag({ flag, children }: { flag: string; children: React.ReactNode }) {
  return flagClient.isEnabled(flag) ? <>{children}</> : <Navigate to="/" replace />;
}
<Route
  path="/new-reports"
  element={
    <RequireFlag flag="release-new-reports">
      <ReportsPage />
    </RequireFlag>
  }
/>

Pair it with React.lazy on ReportsPage and a gated-off feature isn’t even downloaded.

Per-user targeting

Percentage rollouts, beta cohorts, and plan-tier gating all rely on the context you pass as the second argument. The key identifies the entity (a user ID, company slug, etc.) and the other fields are attributes the dashboard can target. The rule itself lives in the dashboard, not in your React code — that’s what lets a product manager change who sees a feature without a deploy.

function Editor({ user }: { user: User }) {
  const showNewEditor = flagClient.isEnabled('experiment-new-editor', {
    key: user.id,
    plan: user.plan,
    country: user.countryCode,
  });
  return showNewEditor ? <NewEditor /> : <ClassicEditor />;
}

You define the rule once — “users where plan equals team AND country equals US” — and adjust it from the dashboard whenever you want.

How flag changes reach the client

The SDK refreshes its cached rules in the background — every 60 seconds by default, backing off on failure up to 15 minutes. When you flip a flag in the dashboard, clients pick up the change on their next refresh, so propagation takes up to about a minute, not instantly. Each isEnabled call reads the latest cached value, so any component that re-renders after a refresh sees the new value automatically.

There’s no push event to subscribe to. The one case where you need a hook is a long-lived view that must reflect a mid-session change (a kill switch, an experiment ending) without waiting for a natural re-render — there you can poll on an interval:

import { useEffect, useState } from 'react';
import { flagClient } from './lib/flaggy';

export function usePolledFlag(key: string, ms = 60_000): boolean {
  const [enabled, setEnabled] = useState(() => flagClient.isEnabled(key));

  useEffect(() => {
    const id = setInterval(() => setEnabled(flagClient.isEnabled(key)), ms);
    return () => clearInterval(id);
  }, [key, ms]);

  return enabled;
}

For everything else, the direct flagClient.isEnabled(...) call is all you need.

Keeping React flags tidy

The wiring above is the easy part. The harder discipline is removing flags once a feature is fully rolled out, so your components don’t fill up with checks that are permanently true. A naming convention and a removal habit are what prevent that — covered in feature flag management.

The same model — one shared client, direct isEnabled calls, dashboard-side targeting — applies to Angular, Vue, and Svelte. See our guides on using feature flags in Angular and feature flags in JavaScript.

Flaggy gives you local-evaluation SDKs, segmentation, and a full audit log on a flat $99/month plan — no per-user metering.