Feature Flags in React: Provider, Hooks, and Real-Time Updates

How to add feature flags to a React app with a provider and a useFeatureFlag hook that re-renders automatically, plus user targeting and tests without the network.

  • react
  • tutorials

Adding a feature flag to a React app is mostly a state-management problem wearing a release-management costume. A flag value lives on a server, it can change while a user is sitting on the page, and when it changes you want exactly the components that read it to re-render with the new value. Do that by hand and you end up writing a fetch, a context, an effect, and a subscription. Do it with a first-party hook and it is two lines: wrap your tree in a provider, read the flag with useFeatureFlag.

This guide builds up the idiomatic version with the Featureflip React SDK: install and provider, reading a boolean flag, handling the loading state so users never see the wrong variant flash, multivariate flags, targeting a logged-in user in real time, and testing components without touching the network. The code targets React 18 and React 19.

Key Takeaways

  • Feature flags in React are external state: wrap your app in <FeatureflipProvider> once, then call useFeatureFlag('flag-key', defaultValue) in any component and it re-renders automatically when the flag changes.
  • The type of the default value picks the variation method, so useFeatureFlag('key', false) reads a boolean, useFeatureFlag('key', 'blue') reads a string, and an object default reads JSON.
  • Gate render on useFeatureflipStatus().isReady to avoid the “flash of the wrong variant” before flags load.
  • A hand-rolled context re-renders every consumer on any change; the SDK uses useSyncExternalStore with a memoized snapshot, so only the component reading the changed flag re-renders.
  • The React SDK is a client-side SDK, billed on Featureflip’s flat plans with unlimited evaluations, so client-side traffic does not meter the bill the way per-MAU pricing does.

1. Why feature flags are awkward in React

The reason rolling your own flag layer feels harder than it should is that React components are a render tree and flags are server state that mutates underneath that tree. The naive version usually looks like a context holding a flags object, hydrated by a useEffect that fetches on mount. It works until you notice three things.

It tears and goes stale. A single context value holding every flag means any update replaces the object reference, so every component consuming that context re-renders even if the one flag it reads did not change. You either accept the over-rendering or start hand-writing per-flag selectors and memoization.

It misses live changes. A fetch on mount reads the flag once. If you flip a flag during an incident, nothing on an open page reacts until the next reload, unless you also build a polling loop or wire up a server-sent-events stream yourself and merge the deltas into state.

It fights React’s own machinery. React 18’s concurrent rendering can read external state at surprising moments, and StrictMode intentionally double-mounts effects in development. A naive useEffect + useState subscription can open two connections, miss the initial value, or read a torn snapshot. The supported answer to “subscribe a component to an external store” is useSyncExternalStore, and wiring it correctly per flag is exactly the boilerplate a first-party hook exists to delete.

The SDK closes all three: one provider owns the connection, updates arrive over a stream, and the hook reads through useSyncExternalStore with a memoized snapshot so re-renders stay surgical.

Components re-rendered when one flag changes A horizontal bar chart comparing how many components re-render when a single flag changes in a tree of twelve flag-reading components. A hand-rolled single-context provider re-renders all twelve consumers. The Featureflip useFeatureFlag hook re-renders only the one component that reads the changed flag. Components re-rendered when one flag changes Illustrative: a tree of 12 flag-reading components, one flag updates Single-context provider 12 re-render useFeatureFlag hook 1 re-renders 0 6 12 components
A single shared context replaces its value on every change, so all consumers re-render. The hook's memoized per-flag snapshot re-renders only the component whose value actually changed.

2. Install the SDK and wrap your app in a provider

The React SDK is a thin set of bindings over the framework-agnostic Browser SDK, so you install both.

Terminal window
npm install @featureflip/react @featureflip/browser

Wrap your component tree once, at the root, in FeatureflipProvider. The provider creates a client, opens the connection on mount, and cleans up on unmount.

import { FeatureflipProvider } from '@featureflip/react';
export function App() {
return (
<FeatureflipProvider clientKey="your-client-sdk-key">
<Dashboard />
</FeatureflipProvider>
);
}

The clientKey is a client SDK key, not a server key. Client keys are safe to ship in a browser bundle because client-side evaluation returns only the results a user is allowed to see, never your full ruleset. If you have not created a project and grabbed a key yet, the JavaScript quickstart walks through it end to end in a few minutes.

One provider per app is enough. If you mount two providers with the same clientKey (a micro-frontend, say), they share a single underlying connection rather than opening two, so there is no penalty for nesting.


3. Read a flag with useFeatureFlag

Inside the provider, any component reads a flag with one hook. Pass the flag key and a default value to fall back to before flags load or if the key is missing.

import { useFeatureFlag } from '@featureflip/react';
function Checkout() {
const newFlow = useFeatureFlag('new-checkout', false);
return newFlow ? <NewCheckout /> : <LegacyCheckout />;
}

That is the whole integration for a boolean gate. When the flag flips in the dashboard, or when a percentage rollout starts serving this user the new variant, Checkout re-renders with the new value on its own. There is no subscription to set up and no event listener to remember to remove. The hook handles the subscribe and unsubscribe through useSyncExternalStore, and because the snapshot is memoized per flag, a component reading new-checkout does not re-render when some other flag changes around it. The full hook and provider API lives in the React SDK reference.


4. Handle the loading state so the wrong variant never flashes

There is a window between mount and the first flag load. During that window every useFeatureFlag call returns its default. If your default is “feature off” and the user should see “feature on,” they get a flash of the old UI before it swaps. For a button color that is harmless. For swapping an entire checkout flow it looks broken.

Gate the part of the tree that depends on flags on the client being ready. useFeatureflipStatus exposes that.

import { useFeatureflipStatus } from '@featureflip/react';
function StatusGate({ children }: { children: React.ReactNode }) {
const { isReady, isError, error } = useFeatureflipStatus();
if (isError) return <ErrorState message={error?.message} />;
if (!isReady) return <Skeleton />;
return <>{children}</>;
}

Wrap the flag-dependent subtree in the gate and it renders a skeleton until real values are in, then renders once with the correct variant.

<FeatureflipProvider clientKey="your-client-sdk-key">
<StatusGate>
<Dashboard />
</StatusGate>
</FeatureflipProvider>

How wide to draw the gate is a judgement call. Gate too much and the whole page waits on flags; gate too little and a flagged region pops in late. A good rule is to gate only the subtree whose default-vs-loaded difference is visually jarring, and let cosmetic flags resolve in place.


5. Multivariate flags: strings, numbers, and JSON

Not every flag is a boolean. A flag can serve one of several string variants, a numeric tuning value, or a whole config object. useFeatureFlag reads all of them through the same call: the type of the default value decides which variation is read.

const enabled = useFeatureFlag('new-checkout', false); // boolean
const color = useFeatureFlag('cta-color', 'blue'); // string
const limit = useFeatureFlag('upload-limit', 100); // number
const layout = useFeatureFlag('dashboard-config', { // JSON object
columns: 2,
density: 'comfortable',
});

The boolean default reads a boolean variation, the string default reads a string variation, the number default reads a number, and an object or array default reads a JSON variation. There is no separate useStringFlag or useJsonFlag to remember. One caution with object defaults: the hook serializes the value to keep its reference stable across renders, so a returned object only changes identity when its contents actually change. That means you can safely put a flag-driven config object straight into a dependency array without triggering an effect on every render.


6. Target a logged-in user in real time

Flags get interesting once they evaluate against who the user is: roll out to internal staff first, gate a plan-tier feature, ship to one beta cohort. That needs an evaluation context, and in a React app the context usually arrives after the initial render, once auth resolves.

The provider takes a context prop. When it changes, the SDK re-identifies the user and re-evaluates every flag, and your components re-render with the user-specific values.

function App() {
const { user } = useAuth();
return (
<FeatureflipProvider
clientKey="your-client-sdk-key"
context={user ? { user_id: user.id, plan: user.plan } : undefined}
>
<StatusGate>
<Dashboard />
</StatusGate>
</FeatureflipProvider>
);
}

Before login the context is undefined and flags evaluate anonymously; the moment user populates, the provider calls identify and a flag with a targeting rule on plan === 'pro' starts returning the pro variant without a reload. If you would rather drive identify imperatively, for example inside a login handler, useFeatureflipClient hands you the client to call client.identify({ ... }) directly. Either way, updates also flow the other direction: with streaming on, a flag change made in the dashboard pushes to open sessions over the stream and the relevant components re-render live.


7. Test components without the network

A flag-gated component should be testable without standing up a server or stubbing fetch. The SDK ships a TestFeatureflipProvider that supplies fixed flag values synchronously, with no network call and no loading delay.

import { TestFeatureflipProvider } from '@featureflip/react';
import { render, screen } from '@testing-library/react';
test('shows the new checkout when the flag is on', () => {
render(
<TestFeatureflipProvider flags={{ 'new-checkout': true }}>
<Checkout />
</TestFeatureflipProvider>,
);
expect(screen.getByText('Place order')).toBeInTheDocument();
});

The test provider reports isReady: true immediately, so a StatusGate resolves on the first render and you assert on the real variant. Write one test per branch by flipping the value in the flags object, and the component’s flag handling is covered without any mocking ceremony.


8. Why use a first-party React SDK instead of rolling your own

You can build a flag context yourself, and for a single boolean that never changes at runtime, you arguably should. The cost shows up as the app grows: per-flag subscriptions so one change does not re-render the page, a streaming connection with delta merging so live flips land, useSyncExternalStore so concurrent React does not tear, and StrictMode handling so development does not open duplicate connections. Each of those is a known footgun, and re-deriving them is the kind of undifferentiated work the build-vs-buy decision is meant to catch.

The pricing angle matters specifically for a client-side SDK. React flags evaluate in the browser, so on a usage-billed platform every user who loads the page is a metered event, and a successful launch that triples traffic also triples the flag bill. Featureflip uses flat pricing with unlimited evaluations on every plan, so client-side traffic does not move the invoice. The pricing page has the full numbers, and the comparison pages break down how that lands against the per-MAU and per-seat vendors for a front-end-heavy app.


The shorter version

Feature flags in a React app are external state, and the idiomatic integration is small. Install @featureflip/react and @featureflip/browser, wrap your tree once in <FeatureflipProvider> with a client key, and read any flag with useFeatureFlag('key', defaultValue). The default value’s type picks the variation, so the same hook reads booleans, strings, numbers, and JSON config. Gate the flag-dependent subtree on useFeatureflipStatus().isReady so users never see the wrong variant flash, pass a context prop to target logged-in users and re-evaluate in real time, and use TestFeatureflipProvider to test every branch with no network. Under the hood the hook reads through useSyncExternalStore with a memoized snapshot, so only the component reading a changed flag re-renders, which is the part a hand-rolled context gets wrong first.


Frequently asked questions

How do I add a feature flag to a React app?

Install @featureflip/react and @featureflip/browser, wrap your component tree in <FeatureflipProvider clientKey="..."> at the root, and call useFeatureFlag('your-flag-key', false) in any component inside the provider. The hook returns the current flag value and re-renders the component automatically when the value changes. That is the entire integration for a boolean gate: a provider at the top and one hook where you branch on the flag.

What does useFeatureFlag return before the flag loads?

It returns the default value you pass as the second argument. There is a short window between mount and the first flag load where every flag reads its default, so choose defaults that are safe to show briefly. To avoid rendering the default variant and then swapping, gate the flag-dependent subtree on useFeatureflipStatus().isReady and render a skeleton until the client reports ready, then render once with the loaded values.

How does the React SDK avoid unnecessary re-renders?

The hook subscribes through React’s useSyncExternalStore and memoizes a per-flag snapshot. A single shared context holding all flags re-renders every consumer whenever any flag changes, because the context value’s reference changes. The SDK instead compares each flag’s value (by identity for primitives, by serialized form for objects) and only notifies the components reading a flag whose value actually changed, so an unrelated flag update does not re-render your component.

Do the hooks work with React Server Components?

The hooks are client-side and run in Client Components, because they read live flag state that updates in the browser. In an app that uses Server Components, mark the components that call useFeatureFlag with 'use client' and keep the FeatureflipProvider in the client tree. If you need to evaluate flags on the server during a render, use a server-side SDK in that code path instead of the React hooks. The SDK supports React 18 and 19 and works with the React Compiler and Suspense.

Can I use feature flags in Next.js, Remix, or Vite with this SDK?

Yes. The React SDK works in any React setup, including Next.js, Remix, Vite, and Create React App, because it builds on the framework-agnostic Browser SDK. The one framework-specific consideration is server rendering: in a server-rendered app, keep the provider and the flag-reading components in the client tree so the hooks run in the browser where flag state is live, and gate on isReady to keep the server-rendered markup and the first client render consistent. For the full server-rendering setup, including how to evaluate on the server to avoid the flash, see the companion guide on adding feature flags to a Next.js app.


Featureflip is a focused, flat-priced feature flag platform with a first-party React SDK, sub-millisecond evaluation, and SSE streaming for live flag updates, so client-side traffic never meters the bill. The React SDK reference has the full API, and you can start on the free Solo plan without a credit card.