How to Add Feature Flags to React in 5 Minutes
Every React application reaches a point where you need to control what users see — without redeploying. Maybe you want to test a new checkout flow with 10% of users. Maybe you need a kill switch for a feature that's causing performance issues. Maybe your PM wants to launch a feature on Tuesday at 9am, not whenever the deploy pipeline finishes.
Feature flags solve all of these problems. They let you wrap components, routes, or entire features behind a toggle that you control from a dashboard — no code changes, no redeployment, no downtime.
If you've ever used an environment variable like NEXT_PUBLIC_ENABLE_NEW_UI and redeployed just to flip it, you already understand the concept. Feature flags are the same idea, but dynamic: you change the value in a dashboard, and every connected client picks it up in real time.
In this guide, we'll go from zero to production-ready React feature flags in 5 minutes.
The Simplest Approach: Environment Variables
Before reaching for a library, many teams start with environment variables:
// feature-flags.ts
export const flags = {
newPricing: process.env.NEXT_PUBLIC_NEW_PRICING === 'true',
darkMode: process.env.NEXT_PUBLIC_DARK_MODE === 'true',
};
// PricingPage.tsx
import { flags } from './feature-flags';
export function PricingPage() {
if (flags.newPricing) {
return <NewPricingTable />;
}
return <LegacyPricingTable />;
}
This works for simple cases, but has serious limitations:
Requires a redeploy to change any flag value
No gradual rollouts — it's all-or-nothing for every user
No user targeting — you can't enable a feature for beta testers only
No instant kill switch — if a feature breaks, you need to redeploy to disable it
No audit trail — who changed what, when?
For anything beyond the simplest use case, you need a feature flag service. Let's set one up.
Quick Start: React Feature Flags in 5 Minutes
We'll use Rollgate, which provides a React-native API with hooks and providers. The entire setup takes three steps.
Step 1: Install the SDK
npm install @rollgate/sdk-react
Step 2: Wrap Your App with the Provider
// app.tsx
import { RollgateProvider } from '@rollgate/sdk-react';
export default function App({ children }: { children: React.ReactNode }) {
const currentUser = useCurrentUser(); // your auth hook
return (
<RollgateProvider
apiKey="YOUR_API_KEY"
user={{ id: currentUser.id, email: currentUser.email, plan: currentUser.plan }}
>
{children}
</RollgateProvider>
);
}
The provider connects to Rollgate's API, fetches your flag configuration, and makes it available to every component via React context. It also opens an SSE connection for real-time updates — when you toggle a flag in the dashboard, every connected client picks it up within seconds.
The user object is optional but recommended. It enables targeting rules (e.g., "enable for users on the Pro plan") and consistent rollout assignments.
Step 3: Use the useFlag Hook
import { useFlag } from '@rollgate/sdk-react';
export function CheckoutPage() {
const showNewCheckout = useFlag('new-checkout', false);
if (showNewCheckout) {
return <NewCheckout />;
}
return <LegacyCheckout />;
}
That's it. Three steps, five minutes.
The useFlag Hook in Depth
The useFlag hook accepts two arguments: the flag key and a default value.
Boolean Flags
The most common type:
const isEnabled = useFlag('dark-mode', false);
String Flags
For A/B testing and multivariate experiments:
const variant = useFlag('checkout-layout', 'control');
// 'control' | 'single-page' | 'multi-step'
Number Flags
For numeric configuration:
const maxItems = useFlag('cart-max-items', 10);
JSON Flags
For complex configuration objects:
const config = useFlag('pricing-config', { currency: 'USD', showAnnual: true });
Default Values Matter
The default value serves two purposes:
Fallback — if the flag doesn't exist or the SDK can't reach the server, your app uses this value instead of crashing
Type inference — TypeScript infers the return type from the default, so
useFlag('dark-mode', false)returnsboolean
Always choose a safe default. If your flag controls an untested feature, default to false.
Conditional Rendering Patterns
Component-Level Gating
Show or hide an entire component:
function SettingsPage() {
const showBetaFeatures = useFlag('beta-features', false);
return (
<div>
<GeneralSettings />
<SecuritySettings />
{showBetaFeatures && <BetaFeatures />}
</div>
);
}
Route-Level Gating
Control access to entire pages:
import { useFlag } from '@rollgate/sdk-react';
import { Navigate } from 'react-router-dom';
function AnalyticsPage() {
const analyticsEnabled = useFlag('analytics-page', false);
if (!analyticsEnabled) {
return <Navigate to="/dashboard" replace />;
}
return <AnalyticsDashboard />;
}
Wrapper Component Pattern
For reusable gating, create a wrapper:
function FeatureGate({
flag,
fallback = null,
children,
}: {
flag: string;
fallback?: React.ReactNode;
children: React.ReactNode;
}) {
const isEnabled = useFlag(flag, false);
return isEnabled ? <>{children}</> : <>{fallback}</>;
}
// Usage
<FeatureGate flag="new-header" fallback={<OldHeader />}>
<NewHeader />
</FeatureGate>
Gradual Rollouts
Gradual rollouts let you release a feature to a percentage of users instead of everyone at once. This is the primary reason most teams adopt feature flags.
How It Works
When you create a flag with a percentage rollout (e.g., 20%), the SDK uses consistent hashing to determine whether a specific user sees the feature:
The same user always gets the same result for a given flag (no flickering)
Different flags hash independently
The distribution is uniform
In your code, nothing changes:
function CheckoutPage() {
// Returns true for 20% of users, false for the rest
const useNewCheckout = useFlag('new-checkout', false);
return useNewCheckout ? <NewCheckout /> : <LegacyCheckout />;
}
When you increase the rollout from 20% to 50%, users who were already seeing the feature continue to see it, and new users are added to the cohort.
Why User IDs Matter
For consistent hashing to work, the SDK needs a stable user identifier:
<RollgateProvider
apiKey="YOUR_API_KEY"
user={{ id: user.id }}
>
{children}
</RollgateProvider>
Without a user ID, the SDK generates a random identifier per session, which means the same person might see different experiences across sessions.
A/B Testing
A/B testing takes gradual rollouts further. Instead of on/off, you assign users to named variants and measure which performs better.
Create a flag with string variants: control (50%), variant-a (25%), variant-b (25%).
function PricingPage() {
const variant = useFlag('pricing-experiment', 'control');
switch (variant) {
case 'variant-a':
return <PricingAnnualFirst />;
case 'variant-b':
return <PricingCompact />;
default:
return <PricingDefault />;
}
}
Tracking Conversions
Track events when users convert:
import { useFlag, useRollgate } from '@rollgate/sdk-react';
function PricingPage() {
const variant = useFlag('pricing-experiment', 'control');
const { track } = useRollgate();
function handleSubscribe(userId: string) {
track({
flagKey: 'pricing-experiment',
eventName: 'subscription-started',
userId,
variationId: variant,
});
}
return <PricingTable variant={variant} onSubscribe={handleSubscribe} />;
}
Feature Flags with Next.js
Next.js adds complexity with Server Components, SSR, and middleware.
Client Components
Identical to standard React — wrap your layout with the provider and use hooks.
Server Components
Use the Node.js SDK for server-side evaluation:
// app/dashboard/page.tsx (Server Component)
import { createClient } from '@rollgate/sdk-node';
const rollgate = createClient({ serverKey: process.env.ROLLGATE_SERVER_KEY! });
export default async function DashboardPage() {
const showNewDashboard = await rollgate.isEnabled('new-dashboard', {
user: { id: getCurrentUserId() },
});
return showNewDashboard ? <NewDashboard /> : <LegacyDashboard />;
}
Middleware
For route-level gating before rendering:
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@rollgate/sdk-node';
const rollgate = createClient({ serverKey: process.env.ROLLGATE_SERVER_KEY! });
export async function middleware(request: NextRequest) {
const userId = request.cookies.get('user_id')?.value;
const betaEnabled = await rollgate.isEnabled('beta-access', {
user: { id: userId },
});
if (request.nextUrl.pathname.startsWith('/beta') && !betaEnabled) {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
}
Best Practices
1. Keep Flag Checks Out of Hot Render Paths
// Bad — hook inside a callback violates Rules of Hooks
function ProductList({ products }) {
return products.map((p) => {
const showBadge = useFlag('new-badge', false); // breaks Rules of Hooks
return <ProductCard key={p.id} showBadge={showBadge} product={p} />;
});
}
// Good — evaluate once at the top level, pass as prop
function ProductList({ products }) {
const showBadge = useFlag('new-badge', false); // called once, at top level
return products.map((p) => (
<ProductCard key={p.id} showBadge={showBadge} product={p} />
));
}
2. Test Both Flag Paths
Every feature flag creates two code paths. Test both by mocking the SDK:
import { useFlag } from '@rollgate/sdk-react';
jest.mock('@rollgate/sdk-react', () => ({
...jest.requireActual('@rollgate/sdk-react'),
useFlag: jest.fn(),
}));
describe('CheckoutPage', () => {
it('renders new checkout when flag is on', () => {
(useFlag as jest.Mock).mockReturnValue(true);
render(<CheckoutPage />);
expect(screen.getByText('New Checkout')).toBeInTheDocument();
});
it('renders legacy checkout when flag is off', () => {
(useFlag as jest.Mock).mockReturnValue(false);
render(<CheckoutPage />);
expect(screen.getByText('Legacy Checkout')).toBeInTheDocument();
});
});
Mocking useFlag directly lets you control flag values in tests without hitting the network.
3. Clean Up Stale Flags
Feature flags are temporary. Once a feature is fully rolled out, remove the flag and the conditional logic. Stale flags create confusion and increase code complexity. Set a review date when you create each flag.
4. Name Flags Descriptively
Use kebab-case and be specific:
Good:
checkout-single-page,pricing-annual-toggle,onboarding-v2Bad:
flag1,test,new-feature,temp
FAQ
How does it affect bundle size? The @rollgate/sdk-react package is under 5 KB gzipped. No impact on build time, and tree-shaking removes unused exports.
Do feature flags cause a flash of content? By default, the SDK renders with default values until the configuration is fetched. To prevent this, use the isReady check from useRollgate() to show a loading state, or use server-side evaluation with the Node SDK in Next.js.
What happens if the API is unavailable? The SDK uses local caching and a circuit breaker. If the API is unreachable, it falls back to the last known values. Your app continues working with the most recent configuration.
Are evaluations counted per render? No. Flag evaluation happens when the SDK fetches the configuration, not on each useFlag call. The hook reads from an in-memory cache — essentially free.
Get Started
Install
@rollgate/sdk-reactWrap your app with
RollgateProviderUse
useFlagto read flag valuesControl rollouts from the dashboard
Rollgate has a free tier with 500K API requests/month, unlimited flags, and all 13 SDKs included. Sign up at rollgate.io — takes 30 seconds, no credit card required.





