Dynamic Configuration in React — Feature Flags Without the Jank

Published: (January 9, 2026 at 01:55 PM EST)
5 min read
Source: Dev.to

Source: Dev.to

The Problem With Feature Flags in React

Most React apps handle configuration one of three ways:

1. Build‑time env vars

const isNewCheckout = process.env.REACT_APP_NEW_CHECKOUT === "true";

function Checkout() {
  return isNewCheckout ?  : ;
}

Change it? Rebuild and redeploy the entire app.

2. Props drilled from the top

function App() {
  const [flags, setFlags] = useState(null);

  useEffect(() => {
    fetch("/api/flags")
      .then((r) => r.json())
      .then(setFlags);
  }, []);

  if (!flags) return ;

  return ;
}

Works, but now you’re threading props through many components. Using Context solves the drilling, but every flag change re‑renders everything.

3. Third‑party SDK with its own patterns

import { useFlags } from "some-feature-flag-sdk";

function Checkout() {
  const { newCheckout } = useFlags();
  // …
}

Better, but now you have a $300 / month bill and a vendor‑specific API.

What Dynamic Configuration Should Feel Like

function Checkout() {
  const isNewCheckout = useConfig("new-checkout");

  return isNewCheckout ?  : ;
}

When someone flips that value in a dashboard:

  • Component re‑renders automatically
  • No page refresh needed
  • No prop drilling
  • Full TypeScript support

Building It With Replane

Replane is open‑source (MIT) and does exactly this.

1. Wrap your app

import { ReplaneProvider } from "@replanejs/react";

function App() {
  return (
    }
    >
      
    
  );
}

The provider connects via Server‑Sent Events. Config loads once, then streams updates in real‑time.

2. Read configs anywhere

import { useConfig } from "@replanejs/react";

function Checkout() {
  const isNewCheckout = useConfig("new-checkout");
  const discountBanner = useConfig("checkout-banner-text");

  return (
    
      {discountBanner && }
      {isNewCheckout ?  : }
    
  );
}

The hook subscribes only to the requested config, so only components that use a particular key re‑render when it changes.

3. Add targeting with context

function Checkout() {
  const { user } = useAuth();

  const rateLimit = useConfig("api-rate-limit", {
    context: {
      userId: user.id,
      plan: user.subscription,
      country: user.country,
    },
  });

  // Premium users might get 10 000, free users get 100
}

Override rules are defined in the Replane dashboard:

  • If plan equals premium → return 10000
  • If country equals DE → return 500
  • Default → 100

No code changes are required when you add new rules.

Making It Type‑Safe

Generic hooks work, but you can tighten the contract:

// config.ts
import { createConfigHook } from "@replanejs/react";

interface AppConfigs {
  "new-checkout": boolean;
  "checkout-banner-text": string | null;
  "api-rate-limit": number;
  "pricing-tiers": {
    free: { requests: number };
    pro: { requests: number };
  };
}

export const useAppConfig = createConfigHook();
// Checkout.tsx
import { useAppConfig } from "./config";

function Checkout() {
  // Autocomplete works, type is inferred
  const isNewCheckout = useAppConfig("new-checkout");
  //    ^? boolean

  const pricing = useAppConfig("pricing-tiers");
  //    ^? { free: { requests: number }; pro: { requests: number } }
}

A typo in the config name? TypeScript catches it. Wrong type assumption? TypeScript catches it.

Handling Loading States

Choose the strategy that fits your app:

Option 1 – Loader prop (default)

}
>
  

Shows a loader until all configs are loaded. Simple, but blocks the whole app.

Option 2 – Suspense

}>
  
    
  

Integrates with React’s Suspense. Ideal if you already use it for data fetching.

Option 3 – Async mode with defaults


  

Renders immediately with the provided defaults. Real values replace them once the connection is established. No loading UI, but values may “flip” after the initial render.

Server‑Side Rendering (SSR) Hydration

// On server
import { Replane, getReplaneSnapshot } from "@replanejs/react";

const replane = new Replane();
await replane.connect({ baseUrl: "...", sdkKey: "..." });
const snapshot = replane.getSnapshot();   // ← server‑fetched snapshot
// Pass `snapshot` to the client via props or serialize it into HTML

// On client

  

The client hydrates immediately from the snapshot, then connects for real‑time updates.

When to Use This

Good fits

  • Feature flags for gradual rollouts
  • Simple A/B‑test variants
  • Per‑user or per‑tenant customization
  • UI text that marketing wants to tweak
  • Operational limits (rate limits, max items, timeouts)
  • Kill switches for incident response

Keep as build‑time config

  • API endpoints (don’t change at runtime)
  • Analytics keys (don’t change at runtime)
  • Anything that affects build output

Common Mistakes

1. Putting everything in dynamic config

Not every value needs real‑time updates. If it doesn’t change while the app is running, keep it static.

2. No defaults

// Bad – crashes if the config server is down
const limit = useConfig("rate-limit");

// Good – works even before the connection is established

  {/* ... */}

3. Context in the wrong place

// Bad – creates a new object each render, breaks memoization
const value = useConfig("limit", { context: { userId: user.id } });

// Better – stable reference
const context = useMemo(() => ({ userId: user.id }), [user.id]);
const value = useConfig("limit", { context });

4. Ignoring error boundaries

import { ErrorBoundary } from "react-error-boundary";

Config failed to load}>
  
    
  

Connection failures throw; catch them with an error boundary.

Getting Started

npm install @replanejs/react

If you’re self‑hosting Replane, point baseUrl to your instance. Otherwise, the free tier is available at cloud.replane.dev.

import { ReplaneProvider, useConfig } from "@replanejs/react";

function App() {
  return (
    Loading…}
    >
      
    
  );
}

function Main() {
  const isEnabled = useConfig("feature-enabled");
  return Feature is {isEnabled ? "on" : "off"};
}

That checkout kill‑switch? It’s now a toggle in the dashboard. Product can flip it themselves. The 10 % rollout? One rule change, no deploy. And when something breaks at 2 am, I disable it from my phone.

Questions? Drop a comment or check out the GitHub repo.

Back to Blog

Related posts

Read more »

Improved environment variables UI

The environment variables UI is now easier to manage across shared and project environment variables. You can spend less time scrolling, use larger hit targets,...

Developer? Or Just a Toolor?

! https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%...

Todo App

Introduction After completing my first logic‑focused project Counters, I wanted to take the next natural step in complexity — not by improving the UI, but by c...