React in 2026: Build a Typed Feature Flag System with Edge Config and Progressive Delivery

Feature flags are no longer just boolean switches hidden in a config file. In 2026, they are a core delivery system for React teams that want to ship safely, run controlled experiments, and decouple deploys from releases.

In this guide, you will build a practical, typed feature flag system for a React app using:

  • React + TypeScript
  • An Edge Config style JSON endpoint
  • Local caching and fail-safe defaults
  • Role and percentage targeting
  • Gradual rollout workflow

By the end, you will have production-ready patterns you can adapt to your own frontend platform.

Why frontend teams need typed flags now

Many teams still treat flags as untyped strings. That usually causes three problems:

  1. Typos in flag names break behavior silently.
  2. Different apps interpret the same flag differently.
  3. Old flags never get cleaned up because ownership is unclear.

A typed contract solves this. Your React app gets compile-time safety, predictable defaults, and easier cleanup after rollout.

Architecture overview

We will use a simple architecture that works well for blogs, SaaS dashboards, and internal tools:

  • Flags source: a JSON document served from edge storage/CDN.
  • React client: fetches flags on startup, validates shape, caches with TTL.
  • Evaluation layer: computes enabled/disabled based on user context.
  • UI hooks: useFlag() and <FeatureGate />.

Step 1: Define a strict flag schema

Start by defining the flags your app supports. Keep names explicit and domain-focused.

// flags/schema.ts
export type FlagKey =
  | 'newCheckoutFlow'
  | 'aiSummaryCard'
  | 'enableTeamBilling'
  | 'navV2';

export type RolloutRule = {
  percentage?: number; // 0..100
  roles?: string[];
  countries?: string[];
};

export type FlagDefinition = {
  enabled: boolean;
  rule?: RolloutRule;
  owner: string;
  expiresOn?: string; // ISO date for cleanup discipline
};

export type FlagSet = Record<FlagKey, FlagDefinition>;

export const defaultFlags: FlagSet = {
  newCheckoutFlow: { enabled: false, owner: 'payments-team' },
  aiSummaryCard: { enabled: false, owner: 'content-team' },
  enableTeamBilling: { enabled: false, owner: 'billing-team' },
  navV2: { enabled: false, owner: 'frontend-platform' }
};

This makes accidental flag usage impossible unless it is declared in the shared type.

Step 2: Build a safe edge-config fetcher

Next, fetch your remote config and merge it with defaults. If the network fails, your app still behaves safely.

// flags/client.ts
import { defaultFlags, FlagSet } from './schema';

const FLAGS_URL = 'https://edge.example.com/config/flags.json';
const CACHE_KEY = 'app_flags_cache_v1';
const TTL_MS = 60_000;

type CachedFlags = { ts: number; data: FlagSet };

function isFlagSet(value: unknown): value is FlagSet {
  if (!value || typeof value !== 'object') return false;
  return Object.keys(defaultFlags).every((k) => k in (value as object));
}

export async function loadFlags(): Promise<FlagSet> {
  const now = Date.now();

  const cachedRaw = localStorage.getItem(CACHE_KEY);
  if (cachedRaw) {
    try {
      const cached: CachedFlags = JSON.parse(cachedRaw);
      if (now - cached.ts < TTL_MS) return cached.data;
    } catch {
      // ignore bad cache
    }
  }

  try {
    const res = await fetch(FLAGS_URL, { cache: 'no-store' });
    if (!res.ok) throw new Error(`Failed to fetch flags: ${res.status}`);

    const remote = await res.json();
    if (!isFlagSet(remote)) throw new Error('Invalid flag schema');

    const merged = { ...defaultFlags, ...remote };
    localStorage.setItem(CACHE_KEY, JSON.stringify({ ts: now, data: merged }));
    return merged;
  } catch {
    return defaultFlags;
  }
}

Notice the fallback behavior: never block app rendering because a config endpoint is down.

Step 3: Add deterministic rollout targeting

For percentage rollouts, you need deterministic bucketing. A user should stay in the same bucket between sessions.

// flags/evaluate.ts
import { FlagDefinition } from './schema';

export type UserContext = {
  userId: string;
  role?: string;
  country?: string;
};

function hashToPercent(input: string): number {
  let h = 2166136261;
  for (let i = 0; i < input.length; i++) {
    h ^= input.charCodeAt(i);
    h += (h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24);
  }
  return Math.abs(h % 100);
}

export function isFlagEnabled(
  key: string,
  flag: FlagDefinition,
  user: UserContext
): boolean {
  if (!flag.enabled) return false;
  if (!flag.rule) return true;

  const { percentage, roles, countries } = flag.rule;

  if (roles?.length && (!user.role || !roles.includes(user.role))) return false;
  if (countries?.length && (!user.country || !countries.includes(user.country))) return false;

  if (typeof percentage === 'number') {
    const bucket = hashToPercent(`${key}:${user.userId}`);
    return bucket < percentage;
  }

  return true;
}

This enables progressive delivery like 5%, then 20%, then 50%, then 100% without random behavior.

Step 4: React provider + hook

Create a provider that loads flags once and exposes a hook for components.

// flags/provider.tsx
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { defaultFlags, FlagKey, FlagSet } from './schema';
import { loadFlags } from './client';
import { isFlagEnabled, UserContext } from './evaluate';

type FlagsContextType = {
  rawFlags: FlagSet;
  enabled: (key: FlagKey, user: UserContext) => boolean;
};

const FlagsContext = createContext<FlagsContextType>({
  rawFlags: defaultFlags,
  enabled: () => false
});

export function FlagsProvider({ children }: { children: React.ReactNode }) {
  const [rawFlags, setRawFlags] = useState<FlagSet>(defaultFlags);

  useEffect(() => {
    loadFlags().then(setRawFlags).catch(() => setRawFlags(defaultFlags));
  }, []);

  const value = useMemo(() => ({
    rawFlags,
    enabled: (key: FlagKey, user: UserContext) => isFlagEnabled(key, rawFlags[key], user)
  }), [rawFlags]);

  return <FlagsContext.Provider value={value}>{children}</FlagsContext.Provider>;
}

export function useFlag(key: FlagKey, user: UserContext) {
  const ctx = useContext(FlagsContext);
  return ctx.enabled(key, user);
}

Step 5: Gate UI and routes cleanly

Now use the hook in real components.

// components/BillingEntry.tsx
import { useFlag } from '../flags/provider';

export function BillingEntry({ user }: { user: { id: string; role: string; country: string } }) {
  const showTeamBilling = useFlag('enableTeamBilling', {
    userId: user.id,
    role: user.role,
    country: user.country
  });

  if (!showTeamBilling) return null;

  return <a href="/billing/team">Team Billing</a>;
}

Keep flag checks close to entry points. Avoid scattering conditions across deep children when possible.

Production rollout playbook

1) Start internal only

Set roles: ["admin", "staff"] first. Validate UX and telemetry before exposing to customers.

2) Add low percentage

Move to percentage: 5, then 20. Monitor key metrics, error rates, and support tickets.

3) Add a kill switch

Every risky flag should be disabled instantly without redeploy. That is the operational value of this system.

4) Expire old flags

Use the expiresOn field and enforce cleanup in CI. Stale flags create long-term complexity debt.

Common mistakes to avoid

  • Flag explosion: too many flags with no owner or expiry.
  • Client-only security: never use frontend flags to enforce permissions.
  • Inconsistent context: different user IDs across services break deterministic rollouts.
  • No observability: track which flag variants users saw during incidents.

Final checklist for your React app

  1. Typed flag keys shared across app packages.
  2. Safe defaults and offline fallback.
  3. Deterministic percentage bucketing.
  4. Role and region targeting support.
  5. Owner + expiry metadata for cleanup.
  6. Kill switch tested in staging and production.

If you implement this pattern, your team can ship React features faster with lower release risk. You get confidence to iterate in small steps instead of betting everything on one big deployment.

That is exactly what modern frontend engineering should optimize for in 2026: velocity with control.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

Privacy Policy · Contact · Sitemap

© 7Tech – Programming and Tech Tutorials