Feature Flags in React Apps: Rollouts Without Redeploy
AI generated
</>
{ }
React · Feature Flags · Release Management
Feature Flags in React Apps
controlled rollouts without a redeploy

Feature flags in React apps decouple the question of when code gets deployed from the question of when a feature actually becomes visible. Instead of risky big bang releases, new functionality can be rolled out step by step, to specific user groups, or switched off instantly with a kill switch, all without a new build.

17 min read Context · LaunchDarkly · Unleash Progressive Rollouts · Testing

1. Why feature flags decouple deployment and release

Feature flags in React apps solve a problem that regularly causes stress in classic deployment workflows: code gets deployed as soon as it's finished, but a feature is only meant to become visible later, for specific user groups, or purely for testing. Without feature flags the only choice often left is between long lived feature branches with painful merge conflicts, or risky big bang releases where everything goes live at once.

With feature flags in React apps, new code gets continuously integrated into the main branch and deployed regularly, but stays hidden behind a switch until the product team actually wants to release the feature to users. This separation of deployment and release is the actual core of any feature flag strategy and considerably reduces the risk of every single deployment, since a rollback simply means flipping a switch rather than rolling back an entire release.

For React applications two paths are available: a self built, lightweight context for simple use cases, or a managed service like LaunchDarkly, Unleash or Flagsmith for more complex rules with audience segmentation. The following sections show both paths with concrete code.

2. Building a minimal feature flag context yourself

For many projects a self built feature flag mechanism is entirely sufficient, especially when only a few flags are active at once and no complex targeting logic is needed. The basic idea: a React context holds an object with flag names and boolean values, loaded from an API or a static configuration when the app starts.


// src/feature-flags/FeatureFlagProvider.tsx
import { createContext, useEffect, useState, type ReactNode } from "react";

type FlagSet = Record<string, boolean>;

interface FeatureFlagContextValue {
  flags: FlagSet;
  isLoading: boolean;
}

export const FeatureFlagContext = createContext<FeatureFlagContextValue>({
  flags: {},
  isLoading: true,
});

export function FeatureFlagProvider({ children }: { children: ReactNode }) {
  const [flags, setFlags] = useState<FlagSet>({});
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    let cancelled = false;

    fetch("/api/feature-flags")
      .then((res) => res.json())
      .then((data: FlagSet) => {
        if (!cancelled) {
          setFlags(data);
          setIsLoading(false);
        }
      })
      .catch(() => {
        // Fail closed: on error, treat all flags as disabled
        if (!cancelled) setIsLoading(false);
      });

    return () => {
      cancelled = true;
    };
  }, []);

  return (
    <FeatureFlagContext.Provider value={{ flags, isLoading }}>
      {children}
    </FeatureFlagContext.Provider>
  );
}

This basic version of feature flags in React loads all relevant flags once when the application starts, from a dedicated API route that can evaluate server side rules such as user role or environment. The fail closed approach in the catch block is deliberate: if loading the flags fails, new, potentially unfinished features should stay disabled, not accidentally become visible to everyone.

3. Consuming feature flags cleanly with useContext

Consuming feature flags in React components should go through a dedicated hook rather than importing the context directly. This encapsulates the logic, allows switching the underlying mechanism later without touching every component, and makes typos in flag names less likely thanks to TypeScript autocompletion.


// src/feature-flags/useFeatureFlag.ts
import { useContext } from "react";
import { FeatureFlagContext } from "./FeatureFlagProvider";

type FlagName = "new-checkout-flow" | "dark-mode" | "ai-search-suggestions";

export function useFeatureFlag(name: FlagName): boolean {
  const { flags } = useContext(FeatureFlagContext);
  return flags[name] ?? false;
}

// Usage inside a component
function CheckoutPage() {
  const useNewCheckout = useFeatureFlag("new-checkout-flow");

  return useNewCheckout ? <NewCheckoutFlow /> : <LegacyCheckoutFlow />;
}

The FlagName string union type ensures that only known flag names are allowed when calling useFeatureFlag, a typo gets caught immediately by the TypeScript compiler instead of silently passing as false at runtime. For feature flags in React projects with many flags, this central type definition pays off because it doubles as documentation of every active flag.

4. Managed services: LaunchDarkly, Unleash and Flagsmith

As soon as feature flags in React apps need to meet more complex requirements, such as audience segmentation by country or subscription tier, real time updates without reloading the page, or an audit log for compliance purposes, a managed service pays off. LaunchDarkly is considered the market leader with a mature dashboard and an SDK for practically every language, but it is also the most expensive of the three options.

Unleash is open source and can be fully self hosted, which is particularly relevant for teams with strict data protection requirements. Flagsmith is positioned between the two on price and also offers a self hosted variant. All three services provide a React SDK that essentially replaces the custom context approach from the previous section, but adds real time updates via server sent events or WebSockets.


// Example using Unleash's official React SDK
import { FlagProvider, useFlag, useVariant } from "@unleash/proxy-client-react";

const unleashConfig = {
  url: "https://unleash.mironsoft.de/api/frontend",
  clientKey: import.meta.env.VITE_UNLEASH_CLIENT_KEY,
  appName: "shop-frontend",
};

function App() {
  return (
    <FlagProvider config={unleashConfig}>
      <CheckoutPage />
    </FlagProvider>
  );
}

function CheckoutPage() {
  const useNewCheckout = useFlag("new-checkout-flow");
  const layoutVariant = useVariant("checkout-layout-experiment");

  return useNewCheckout ? (
    <NewCheckoutFlow layout={layoutVariant.name} />
  ) : (
    <LegacyCheckoutFlow />
  );
}

The decisive advantage of these SDKs for feature flags in React over a custom context: changes in the dashboard get pushed live to every connected client without an app reload, which considerably speeds up kill switch scenarios during acute production issues. The downside is an additional external dependency and, for hosted variants, ongoing costs per user or request.

5. Progressive rollouts: percentage rollout and targeting

A central use case for feature flags in React apps is the progressive rollout: a new feature first gets enabled for 5 percent of users, then gradually increased to 25, 50 and finally 100 percent, while error rates and metrics are monitored. This pattern drastically reduces the blast radius of a faulty feature compared to a classic all or nothing release.

Managed services usually calculate cohort membership for the percentage via a consistent hash of the user ID, so the same user gets the same flag value on every call, instead of being randomly reassigned on every page visit. This matters to avoid an inconsistent user experience within a session.


// Simplified consistent-hash rollout logic (illustrates what SDKs do internally)
async function sha256Hex(input: string): Promise<string> {
  const data = new TextEncoder().encode(input);
  const hashBuffer = await crypto.subtle.digest("SHA-256", data);
  return Array.from(new Uint8Array(hashBuffer))
    .map((b) => b.toString(16).padStart(2, "0"))
    .join("");
}

async function isInRollout(userId: string, flagName: string, percentage: number): Promise<boolean> {
  const hash = await sha256Hex(`${flagName}:${userId}`);
  // Take the first 8 hex chars as a number between 0 and 0xFFFFFFFF
  const bucket = parseInt(hash.slice(0, 8), 16) / 0xffffffff;
  return bucket < percentage / 100;
}

// Same user always lands in the same bucket for this flag
const enabled = await isInRollout("user-4711", "new-checkout-flow", 25);

For feature flags in React apps with a self built system, this logic can be implemented server side and the result communicated to the frontend application via the API, instead of computing it in the client. Managed services already handle this calculation fully and additionally offer targeting filters by attributes such as country, device type or customer segment right in the dashboard.

6. Feature flags in tests: forcing deterministic behavior

Without special care, feature flags in React apps lead to non deterministic tests, because a test runs one code path or another depending on the external flag state. The solution is to explicitly populate the feature flag context in tests with fixed values, instead of allowing real network calls.


// CheckoutPage.test.tsx — deterministic feature flag values in tests
import { render, screen } from "@testing-library/react";
import { FeatureFlagContext } from "../feature-flags/FeatureFlagProvider";
import { CheckoutPage } from "./CheckoutPage";

function renderWithFlags(flags: Record<string, boolean>) {
  return render(
    <FeatureFlagContext.Provider value={{ flags, isLoading: false }}>
      <CheckoutPage />
    </FeatureFlagContext.Provider>
  );
}

test("shows the new checkout flow when the flag is enabled", () => {
  renderWithFlags({ "new-checkout-flow": true });
  expect(screen.getByTestId("new-checkout-flow")).toBeInTheDocument();
});

test("falls back to the legacy checkout when the flag is disabled", () => {
  renderWithFlags({ "new-checkout-flow": false });
  expect(screen.getByTestId("legacy-checkout-flow")).toBeInTheDocument();
});

This pattern ensures that both code paths of a feature flags in React setup actually get tested, regardless of an external service's state. As soon as a feature flag gets removed, the corresponding tests for the disabled state should be removed too, which leads directly to the next topic: the orderly cleanup of old flags.

7. Avoiding feature flag debt: lifecycle and cleanup

The biggest practical downside of feature flags in React apps doesn't show up when introducing them, but when forgetting about them. Every flag that stays in the code after a complete rollout adds a conditional branch that nobody needs anymore, but that every developer still has to read and understand. This phenomenon is aptly called feature flag debt and, in large codebases, tends to grow unnoticed.

A clear lifecycle helps against this: every new flag gets a planned expiry date and a responsible owner recorded in the ticketing system when it's created. Automated reports that list every flag that has been at 100 percent for more than 90 days actively remind teams to remove the conditional code and delete the flag itself from the managed service. Without this discipline, feature flags in React projects typically accumulate dozens of dead flags that make the codebase unnecessarily complex.

8. Server side flags in React server components

With React server components, an interesting opportunity arises for feature flags in React apps: flag evaluation can happen entirely on the server, before any HTML is even sent to the client. This avoids a visible layout jump that occurs with client side evaluation, when the default state is rendered first and then re-rendered after the flags load.

Since server components run again on every request, the flag lookup can be integrated directly into the component without needing an additional loading state on the client. The disabled feature never appears in the initially rendered HTML this way, which additionally prevents technically savvy users from finding hints of not yet released functionality in the HTML source.

9. Custom build versus managed service compared

The choice between a self built feature flag system and a managed service depends on team size, rule complexity and budget. The table below compares the key properties.

Criterion Custom context Managed service
Setup effort Very low Account and SDK integration needed
Real time updates Only with custom WebSocket logic Built in via SSE/WebSocket
Audience targeting Has to be implemented yourself Configurable in the dashboard
Cost No additional cost Per user or request, except Unleash self hosted
Audit log Has to be built yourself Included by default

For small teams with a handful of simple flags, the custom context is often the more pragmatic starting point for feature flags in React apps, without taking on an external dependency. As soon as audience segmentation, real time kill switches or compliance requirements such as an audit log become important, the advantages of a managed service clearly outweigh the effort, especially with Unleash as a self hostable open source option.

Mironsoft

Feature flag architecture and controlled rollouts for React apps

Want to build low risk releases with feature flags?

We design a fitting feature flag system for your React app, whether a lightweight custom context or integrating a managed service with progressive rollouts and a kill switch.

Architecture consulting

Deciding between custom build and managed service based on your requirements

SDK integration

Integrating LaunchDarkly, Unleash or Flagsmith cleanly into your React app

Flag lifecycle

Establishing processes against feature flag debt and cleaning up existing clutter

10. Summary

Feature flags in React apps separate the technical question of deployment from the product decision of when a feature becomes visible to users. A self built context is entirely sufficient for simple use cases and avoids external dependencies, while managed services like LaunchDarkly, Unleash and Flagsmith offer clear advantages for complex audience targeting and real time updates.

Progressive rollouts via consistent hashing drastically reduce the blast radius of faulty features, while deterministic testing with fixed flag values prevents the test suite from depending on external state. The most important organizational aspect remains the lifecycle: without planned cleanup, every project with feature flags in React quickly accumulates dead flags that unnecessarily bloat the codebase.

Feature Flags in React Apps at a Glance

Deployment versus release

Feature flags decouple when code goes live from the decision of when a feature becomes visible.

Custom context versus managed service

A React context suffices for simple cases, complex targeting logic favors LaunchDarkly, Unleash or Flagsmith.

Progressive rollouts

Consistent hashing of the user ID ensures stable cohort assignment across multiple sessions.

Flag lifecycle

Planned expiry dates and automated reports prevent feature flag debt in growing codebases.

11. FAQ: Feature Flags in React

1Deployment versus release with feature flags?
Deployment means code runs, release means it's visible. Feature flags separate the two.
2When is a custom context enough?
With a few simple flags without complex targeting, a custom context is often the most pragmatic solution.
3When does LaunchDarkly pay off?
With audience targeting, real time updates or audit log needs, the advantages clearly outweigh the effort.
4What is a progressive rollout?
Gradual activation for a growing percentage of users while error rates are monitored.
5Why consistent hashing?
Without it, the same user would randomly get different flag values, causing an inconsistent experience.
6How to test feature flags reliably?
Populate the context in tests with fixed values instead of allowing real network calls.
7What is feature flag debt?
Forgotten flags left in the code after a complete rollout that make the codebase unnecessarily complex.
8How do you prevent feature flag debt?
Planned expiry dates per flag and automated reports that remind the team to clean up.
9Advantage in server components?
Evaluation happens entirely server side, avoiding a visible layout jump in the client.
10Is Unleash a good alternative?
Yes, especially with strict data protection needs, since it is open source and fully self hostable.