Betterflag
Back to blog

How to Add Feature Flags in React

Add feature flags in React without a flash of the wrong UI: server evaluation, a useFlag hook, targeting, percentage rollouts, and tests. Copy-paste patterns for 2026.

Mehdi
August 18, 2026
#feature-flags#react#tutorial#sdk#rollouts

The failure mode of feature flags in React is a flash of the old UI. The component mounts, useEffect fetches a flag, 200ms later the new checkout appears. Users see a jump. Crawlers see the old tree. Your "10% rollout" is a 10% rollout of people who waited.

The fix is to treat a flag like data you already had on first paint: evaluate it on the server (or read a bootstrap snapshot), then render.

If you need the conceptual background first, read what feature flags are. This post is the React-shaped version.

The pattern that works

import { createBetterFlag } from "@betterflag/sdk";
const flags = createBetterFlag({
apiKey: process.env.BETTERFLAG_SDK_KEY!,
environment: "production",
});
export async function Checkout({ user }: { user: { id: string; plan: string } }) {
const showV2 = await flags.isEnabled("checkout-v2", {
userId: user.id,
attributes: { plan: user.plan },
});
return showV2 ? <CheckoutV2 /> : <CheckoutV1 />;
}

This is a Server Component. The boolean exists before HTML leaves the server. No spinner, no flash, no extra round trip. If you are on Next.js App Router, the longer version is feature flags in Next.js.

Client-only React (Vite, CRA, a mobile WebView) cannot do that. Then you bootstrap:

// main.tsx
const snapshot = await flags.loadSnapshot(); // once, before render
createRoot(document.getElementById("root")!).render(
<FlagProvider snapshot={snapshot}>
<App />
</FlagProvider>,
);
import { useFlag } from "@betterflag/react";
export function Checkout() {
const showV2 = useFlag("checkout-v2");
return showV2 ? <CheckoutV2 /> : <CheckoutV1 />;
}

useFlag reads memory. It does not fetch. If your current SDK fetches inside the hook, wrap it so the fetch happens once at boot, not per component.

What not to do

// Causes a flash. Also fires N times if N components call it.
function Checkout() {
const [showV2, setShowV2] = useState(false);
useEffect(() => {
fetch("/api/flags/checkout-v2")
.then((r) => r.json())
.then((d) => setShowV2(d.enabled));
}, []);
return showV2 ? <CheckoutV2 /> : <CheckoutV1 />;
}

Problems: first paint is always v1, loading waterfalls, every mount is a request, and a 10% rollout flickers as the fetch resolves. This is the pattern I see in production more than any other.

Targeting and percentage rollouts

Pass a stable user id. Percentage assignment hashes on that id so the same user stays in the same bucket. Anonymous users can use a cookie id you set on first visit. Do not hash on IP; shared NATs will clump.

const showV2 = await flags.isEnabled("checkout-v2", {
userId: user.id,
attributes: {
plan: user.plan,
country: user.country,
appVersion: APP_VERSION,
},
});

Keep attributes boring: plan, country, version. Feature flag best practices is the longer list. For the rollout itself, percentage rollouts covers ramp schedules and kill criteria.

Client SDK keys are not admin keys

The key you ship to the browser can only evaluate flags. It cannot create them, change targeting, or pull a kill switch. Those are server or MCP operations, authenticated with a different key.

Never put an agent key or a management token in VITE_* / NEXT_PUBLIC_*. Evaluation keys are designed to be public-ish (they still identify your project, so restrict by origin if your vendor supports it). Management keys are secrets. See flags vs environment variables for where each kind of config lives.

Testing both trees

import { render, screen } from "@testing-library/react";
import { FlagProvider } from "@betterflag/react";
import { Checkout } from "./Checkout";
it("renders v1 when checkout-v2 is off", () => {
render(
<FlagProvider overrides={{ "checkout-v2": false }}>
<Checkout />
</FlagProvider>,
);
expect(screen.getByRole("form", { name: /checkout/i })).toHaveAttribute(
"data-variant",
"v1",
);
});
it("renders v2 when checkout-v2 is on", () => {
render(
<FlagProvider overrides={{ "checkout-v2": true }}>
<Checkout />
</FlagProvider>,
);
expect(screen.getByRole("form", { name: /checkout/i })).toHaveAttribute(
"data-variant",
"v2",
);
});

If your SDK has no overrides prop, wrap useFlag yourself in tests. Do not hit a live API from CI.

Kill switch from the same boolean

The React code does not change when you kill a feature. You set checkout-v2 to off. The next evaluation returns false, Server Components send v1, client snapshots refresh. That is the point of a kill switch: the off path is already in the bundle.

If the off path was deleted "because we are at 100%," you do not have a kill switch. You have a comment that used to be a flag. Delete flags on purpose, after you delete the dead branch, not before.

A React-specific checklist

  • Evaluate before first paint (server or bootstrap snapshot).
  • One provider, not fetch-per-component.
  • Stable user id for percentage rollouts.
  • Client key evaluates only. Management keys stay on the server.
  • Tests cover on and off.
  • The off component still exists until you retire the flag.

That is enough React. The Next.js App Router version adds middleware, revalidate, and edge. Feature flags in Next.js is next if that is your stack.

Betterflag's React SDK follows this shape: server isEnabled, client useFlag on a snapshot, MCP for the write path so you are not clicking a dashboard to create checkout-v2. Join the waitlist if you want it; alpha is 50% off for life.

FAQ

How do I add feature flags in React?
Evaluate the flag on the server or at the edge, pass the boolean into the tree as a prop or via a small provider, and branch in render. Avoid fetching the flag in useEffect: that causes a flash of the old UI. Client hooks should read from a cached snapshot, not from a network call on every mount.
How do I prevent a flash of the wrong variant?
Decide the flag before the first paint. In a React Server Component or a Next.js loader, evaluate the flag and pass the result down. If you must evaluate on the client, block first paint with a cached bootstrap payload (the same pattern analytics tools use) rather than a loading spinner that reveals the old UI.
Should I use a feature flag SDK on the client or the server?
Prefer the server for anything that changes what HTML you send, what price you show, or what a user is allowed to do. Use the client SDK for purely interactive UI that cannot be decided up front, and keep the client payload small. Secrets never go in the client SDK key.
How do I test React components that use feature flags?
Inject an override in tests so you do not depend on a live service. Render once with the flag off and once with it on. Snapshot tests that only cover the on path will hide bugs in the path production is actually serving during a rollout.