How to Add Feature Flags to React in 5 Minutes
Why Feature Flags in React?
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 in React 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. We'll start with the simplest possible approach (environment variables), show its limitations, and then implement a proper solution with Rollgate that supports gradual rollouts, A/B testing, and user targeting.
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 it 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 library. Let's set one up.
Quick Start: React Feature Flags in 5 Minutes
We'll use @rollgate/sdk-react, which gives you 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 RollgateProvider
// app.tsx (or _app.tsx, layout.tsx — wherever your root component lives)
import { RollgateProvider } from '@rollgate/sdk-react';
export default function App({ children }: { children: React.ReactNode }) {
return (
<RollgateProvider
clientKey="YOUR_CLIENT_KEY"
user={{ id: currentUser.id, email: currentUser.email, plan: currentUser.plan }}
>
{children}
</RollgateProvider>
);
}
The RollgateProvider connects to Rollgate's edge API, fetches your flag configuration, and makes it available to every component in the tree 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 this flag for users on the Pro plan") and consistent rollout assignments (the same user always sees the same variant).
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, and you have feature flags in React. The useFlag hook returns the flag's current value and re-renders the component when it changes.
Now let's go deeper.
The useFlag Hook
The useFlag hook is the primary way to read feature flag values in React components. It accepts two arguments: the flag key and a default value.
const value = useFlag('flag-key', defaultValue);
Boolean Flags
The most common type. Returns true or false:
const isEnabled = useFlag('dark-mode', false);
String Flags
Useful for A/B testing and multivariate experiments:
const variant = useFlag('checkout-layout', 'control');
// variant: 'control' | 'single-page' | 'multi-step'
Number Flags
For numeric configuration like rate limits or thresholds:
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 value, so
useFlag('dark-mode', false)returnsbooleananduseFlag('variant', 'control')returnsstring
Always choose a safe default. If your flag controls a new, untested feature, default to false. If it controls a critical flow, default to the value that keeps the existing behavior.
The useFlags Hook
If you need multiple flags at once, useFlags returns all flags as an object:
import { useFlags } from '@rollgate/sdk-react';
export function Dashboard() {
const flags = useFlags();
return (
<div>
{flags['new-sidebar'] && <NewSidebar />}
{flags['analytics-v2'] && <AnalyticsV2 />}
</div>
);
}
Conditional Rendering with Feature Flags
React feature flags shine in conditional rendering. Here are the most common patterns.
Component-Level Gating
The simplest pattern — 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 based on a flag:
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 />;
}
Prop-Level Gating
Pass flag values as props to control component behavior without the child knowing about feature flags:
function ProductPage() {
const showReviews = useFlag('product-reviews', false);
const maxImages = useFlag('product-gallery-max', 4);
return (
<ProductDetail
showReviews={showReviews}
maxGalleryImages={maxImages}
/>
);
}
Wrapper Component Pattern
For reusable gating, create a wrapper:
import { useFlag } from '@rollgate/sdk-react';
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 in React
Gradual rollouts let you release a feature to a percentage of users instead of everyone at once. This is one of the most powerful feature flag patterns and the primary reason teams adopt feature flags.
How It Works
When you create a flag in Rollgate with a percentage rollout (e.g., 20%), the SDK uses consistent hashing to determine whether a specific user should see the feature. The hash is based on the user ID and the flag key, which means:
- The same user always gets the same result for a given flag (no flickering between sessions)
- Different flags hash independently (being in the 20% for flag A doesn't mean you're in the 20% for flag B)
- The distribution is uniform — 20% means roughly 20% of users, not exactly 20% of requests
Setting Up a Gradual Rollout
In the Rollgate dashboard, set your flag's rollout to a percentage. In your React code, nothing changes:
function CheckoutPage() {
// This might return true for 20% of users and false for the rest
const useNewCheckout = useFlag('new-checkout', false);
return useNewCheckout ? <NewCheckout /> : <LegacyCheckout />;
}
The SDK handles everything. When you increase the rollout from 20% to 50%, users who were already seeing the feature continue to see it (no disruption), and new users are added to the cohort.
Why User IDs Matter
For consistent hashing to work, the SDK needs a stable user identifier. This is why passing a user object to RollgateProvider is important for rollouts:
<RollgateProvider
clientKey="YOUR_CLIENT_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 the feature in one session and not in another.
A/B Testing with React Feature Flags
A/B testing with feature flags takes gradual rollouts a step further. Instead of just on/off, you assign users to named variants and measure which performs better.
Setting Up an A/B Test
Create a flag in Rollgate with string variants:
control— 50% of users (the existing experience)variant-a— 25% of usersvariant-b— 25% of users
Then use it in React:
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
To measure the impact of your experiment, 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(plan: string) {
track('subscription-started', { plan, variant });
// ... rest of subscribe logic
}
return (
<PricingTable
variant={variant}
onSubscribe={handleSubscribe}
/>
);
}
The Rollgate dashboard shows conversion rates per variant, so you can see which pricing layout drives more subscriptions.
Feature Flags with Next.js
Next.js adds complexity with Server Components, server-side rendering, and middleware. Here's how to handle each.
Client Components
For client components, the setup is identical to standard React. Wrap your layout with RollgateProvider and use hooks:
// app/layout.tsx
import { RollgateProvider } from '@rollgate/sdk-react';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<RollgateProvider clientKey={process.env.NEXT_PUBLIC_ROLLGATE_KEY!}>
{children}
</RollgateProvider>
</body>
</html>
);
}
Server Components
For Next.js Server Components, use the Node.js SDK instead of the React SDK. The Node SDK evaluates flags server-side, which means no client-side flash of content:
// 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 Pattern
For route-level gating in Next.js middleware:
// middleware.ts
import { createClient } from '@rollgate/sdk-node';
const rollgate = createClient({ serverKey: process.env.ROLLGATE_SERVER_KEY! });
export async function middleware(request: NextRequest) {
const userId = getUserIdFromCookie(request);
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));
}
}
This pattern lets you gate entire routes before they render, which is cleaner than checking flags inside every page component.
Feature Flags with React Native
If you're building a mobile app with React Native, Rollgate provides @rollgate/sdk-react-native:
npm install @rollgate/sdk-react-native
import { RollgateProvider, useFlag } from '@rollgate/sdk-react-native';
function App() {
return (
<RollgateProvider
clientKey="YOUR_CLIENT_KEY"
user={{ id: user.id, platform: 'ios' }}
>
<MainNavigator />
</RollgateProvider>
);
}
function HomeScreen() {
const showNewOnboarding = useFlag('mobile-onboarding-v2', false);
// ...
}
The API is nearly identical to @rollgate/sdk-react. The React Native SDK handles mobile-specific concerns like background/foreground state, offline caching, and network reconnection. See the React Native SDK docs for details.
Best Practices for React Feature Flags
1. Keep Flag Checks Out of Hot Render Paths
Feature flag evaluation is fast, but in components that render hundreds of times per second (e.g., list items, animation frames), avoid calling useFlag inside the loop:
// Bad — evaluates the flag on every list item render
function ProductList({ products }: { products: Product[] }) {
return products.map((p) => {
const showBadge = useFlag('new-badge', false); // Called N times
return <ProductCard key={p.id} showBadge={showBadge} product={p} />;
});
}
// Good — evaluate once, pass as prop
function ProductList({ products }: { products: Product[] }) {
const showBadge = useFlag('new-badge', false); // Called once
return products.map((p) => (
<ProductCard key={p.id} showBadge={showBadge} product={p} />
));
}
2. Memoize Components Behind Flags
When a flag controls which component renders, use React.memo or useMemo to prevent unnecessary re-renders when the flag value hasn't changed:
function Dashboard() {
const variant = useFlag('dashboard-layout', 'default');
const content = useMemo(() => {
switch (variant) {
case 'compact': return <CompactDashboard />;
case 'wide': return <WideDashboard />;
default: return <DefaultDashboard />;
}
}, [variant]);
return <Layout>{content}</Layout>;
}
3. Test Both Flag Paths
Every feature flag creates two code paths. Test both:
// __tests__/CheckoutPage.test.tsx
import { render, screen } from '@testing-library/react';
import { RollgateProvider } from '@rollgate/sdk-react';
import { CheckoutPage } from './CheckoutPage';
describe('CheckoutPage', () => {
it('renders new checkout when flag is on', () => {
render(
<RollgateProvider clientKey="test" overrides={{ 'new-checkout': true }}>
<CheckoutPage />
</RollgateProvider>
);
expect(screen.getByText('New Checkout')).toBeInTheDocument();
});
it('renders legacy checkout when flag is off', () => {
render(
<RollgateProvider clientKey="test" overrides={{ 'new-checkout': false }}>
<CheckoutPage />
</RollgateProvider>
);
expect(screen.getByText('Legacy Checkout')).toBeInTheDocument();
});
});
The overrides prop on RollgateProvider lets you set specific flag values in tests without hitting the network.
4. 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, increase code complexity, and make your flag dashboard cluttered. Set a review date when you create each flag.
5. Name Flags Descriptively
Use kebab-case and be specific about what the flag controls:
- Good:
checkout-single-page,pricing-annual-toggle,onboarding-v2 - Bad:
flag1,test,new-feature,temp
Common Patterns
Feature-Gated Routes
Combine React Router with feature flags to gate entire sections of your app:
import { useFlag } from '@rollgate/sdk-react';
function AppRoutes() {
const analyticsEnabled = useFlag('analytics-module', false);
const reportsEnabled = useFlag('reports-module', false);
return (
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
{analyticsEnabled && (
<Route path="/analytics/*" element={<AnalyticsModule />} />
)}
{reportsEnabled && (
<Route path="/reports/*" element={<ReportsModule />} />
)}
<Route path="*" element={<NotFound />} />
</Routes>
);
}
Permission-Based Rendering
Combine feature flags with user permissions for fine-grained access control:
function AdminPanel() {
const advancedAdmin = useFlag('advanced-admin-tools', false);
const { user } = useAuth();
return (
<div>
<UserManagement />
{user.role === 'admin' && advancedAdmin && <AdvancedTools />}
{user.role === 'superadmin' && <SystemConfig />}
</div>
);
}
Beta Programs
Let users opt into beta features, then use feature flags to control the experience:
function BetaOptIn() {
const betaAvailable = useFlag('beta-program-open', false);
const { user, updateUser } = useAuth();
if (!betaAvailable) return null;
return (
<Card>
<CardHeader>
<CardTitle>Beta Program</CardTitle>
</CardHeader>
<CardContent>
<p>Get early access to new features.</p>
<Switch
checked={user.betaOptIn}
onCheckedChange={(checked) => updateUser({ betaOptIn: checked })}
/>
</CardContent>
</Card>
);
}
When a user opts in, their betaOptIn attribute is sent to Rollgate via the user context, and targeting rules can enable beta features specifically for them.
Loading States
Handle the initial SDK load gracefully:
import { useRollgate } from '@rollgate/sdk-react';
function App() {
const { isReady } = useRollgate();
if (!isReady) {
return <Skeleton className="h-screen w-full" />;
}
return <MainApp />;
}
The isReady flag becomes true once the SDK has fetched the initial flag configuration. This prevents a flash of default values before the real values arrive.
FAQ
How do React feature flags affect bundle size?
The @rollgate/sdk-react package is lightweight — under 5 KB gzipped. It uses the browser SDK under the hood and adds only the React-specific bindings (provider, hooks). There is no impact on your build time, and tree-shaking removes any unused exports.
Do feature flags cause a flash of content?
By default, the SDK renders with default values until the flag configuration is fetched. This can cause a brief flash. To prevent it, use the isReady check from useRollgate() to show a loading state, or use server-side evaluation with the Node SDK in Next.js Server Components.
Can I use feature flags with React Server Components?
Yes. Use @rollgate/sdk-node for Server Components and @rollgate/sdk-react for Client Components. See the Next.js section above.
How do feature flags work with SSR?
For server-side rendering, evaluate flags on the server with @rollgate/sdk-node and pass the results as props to your client components. This ensures the initial HTML matches the flag state and prevents hydration mismatches.
What happens if the Rollgate API is unavailable?
The SDK uses local caching and a circuit breaker pattern. If the API is unreachable, it falls back to the last known flag values (cached in memory and optionally in localStorage). Your app continues to work with the most recent flag configuration. The default value you pass to useFlag is used only on the very first load if no cached values exist.
Are feature flag evaluations counted per render?
No. Flag evaluation happens when the SDK fetches the configuration, not on each useFlag call. The hook simply reads from an in-memory cache, so calling it is essentially free — comparable to reading from React context.
What's Next?
You now have everything you need to implement feature flags in React. To recap:
- Install
@rollgate/sdk-react - Wrap your app with
RollgateProvider - Use
useFlagto read flag values - Use the dashboard to control rollouts, run A/B tests, and target users
If you're new to feature flags, start with the fundamentals: What Are Feature Flags? covers the core concepts, Gradual Rollouts Guide explains percentage-based releases in depth, and A/B Testing with Feature Flags walks through experimentation patterns.
For the full React SDK API reference, see the React SDK documentation.
Ready to get started? Create a free Rollgate account — it takes 30 seconds, no credit card required.