React

Ship config changes to your React app without a redeploy

Move feature toggles, copy, limits, and kill switches out of your bundle and into BetterConfig, then read them at runtime over an edge-cached API. Includes a Next.js App Router pattern and a plain client-only fetch.

9 min read

Most React apps hardcode the values that change most often. A feature toggle is a boolean in the bundle. The promo banner copy is a string in a component. The upload limit is a constant. The kill switch for a flaky integration is a commented-out line waiting for someone to notice the incident. Every one of those changes means editing code, opening a pull request, waiting for CI, and shipping a deploy.

Remote config decouples those values from your deploy. You store them outside the bundle, read them at runtime, and change them live. This guide shows how to wire BetterConfig into a React app, with a Next.js App Router pattern as the primary example and a plain client-only variant for apps that are not on Next.

What belongs in remote config

Remote config is the right home for values that are operational rather than structural. Good candidates:

  • Feature toggles. Turn a code path on or off without a release.
  • Copy and content. Banner text, empty-state messages, marketing strings.
  • Limits and tuning. Upload sizes, page sizes, retry counts, timeouts.
  • Promo banners. Schedule and swap announcements without touching the build.
  • Kill switches. Disable a misbehaving integration in seconds during an incident.

These are not feature flags with targeting rules or percentage rollouts yet. BetterConfig ships per-environment JSON values today. Targeted feature flags are on the roadmap.

Coming soon Feature flags with targeting rules and percentage rollouts, plus typed schemas with generated client types.

Step 1: Store the values

In BetterConfig, create a config key and give it a JSON value. Values are stored per environment, so dev, staging, and prod each hold their own copy of the same key. Edit them in the in-app JSON editor, and every change is versioned per environment with full history and one-click rollback.

For this guide, assume a project with slug acme-web and a prod environment holding a few keys.

Step 2: Read them over the API

Published config is served from an edge-cached read API on Cloudflare Workers. You read it with a single authenticated GET:

curl "https://api-public.betterconfig.dev/v1/config?project=acme-web&env=prod" \
  -H "Authorization: Bearer $BC_READ_TOKEN"

The response is a small JSON document:

{
  "projectSlug": "acme-web",
  "environmentKey": "prod",
  "version": 14,
  "fetchedAt": 1749480000000,
  "values": {
    "promo_banner_enabled": true,
    "promo_banner_text": "Summer sale: 20% off",
    "max_upload_mb": 50,
    "checkout_v2": false
  }
}

The fields you care about are values (your keys) and version (a monotonic counter that bumps on every publish). The response also sends an ETag of "v<version>" and Cache-Control: public, max-age=30, stale-while-revalidate=300, so repeat reads are cheap and a conditional request with If-None-Match gets a fast 304.

Step 3: Wire it into React with Context

The cleanest pattern in the Next.js App Router is to fetch config once in a Server Component, pass the values down into a client provider, and expose them through a hook. The server fetch keeps your read token off the client and lets Next cache the result.

Fetch in the root layout (Server Component)

Set next: { revalidate: 30 }so Next's data cache holds the config for the same 30 seconds the edge API caches it. Fail open to an empty object so a transient config error never blocks your render.

// app/layout.tsx (Server Component)
import { ConfigProvider } from "./config-provider";

async function getConfig() {
  const res = await fetch(
    "https://api-public.betterconfig.dev/v1/config?project=acme-web&env=prod",
    {
      headers: { Authorization: `Bearer ${process.env.BC_READ_TOKEN}` },
      // Cache the config for 30s, matching the read API's edge cache window.
      next: { revalidate: 30 },
    },
  );

  if (!res.ok) {
    // Fail open to safe defaults rather than blocking render on config.
    return {};
  }

  const data = await res.json();
  return data.values as Record<string, unknown>;
}

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const config = await getConfig();

  return (
    <html lang="en">
      <body>
        <ConfigProvider config={config}>{children}</ConfigProvider>
      </body>
    </html>
  );
}

Provide it through Context

The provider is a small client component built on createContext and useContext. It exposes a useConfig() hook for the whole object and a useFlag(key, fallback) helper for a single value with a safe default.

// app/config-provider.tsx
"use client";

import { createContext, useContext } from "react";

type Config = Record<string, unknown>;

const ConfigContext = createContext<Config>({});

export function ConfigProvider({
  config,
  children,
}: {
  config: Config;
  children: React.ReactNode;
}) {
  return (
    <ConfigContext.Provider value={config}>{children}</ConfigContext.Provider>
  );
}

export function useConfig() {
  return useContext(ConfigContext);
}

// Read a single key. Falls back to `fallback` when the key is missing.
export function useFlag<T = boolean>(key: string, fallback: T): T {
  const config = useContext(ConfigContext);
  return key in config ? (config[key] as T) : fallback;
}

Consume it in a component

Any client component can now read config without prop drilling. When you publish a new value, the component renders the new value on the next request inside the cache window.

// app/components/promo-banner.tsx
"use client";

import { useFlag } from "../config-provider";

export function PromoBanner() {
  const showBanner = useFlag("promo_banner_enabled", false);
  const message = useFlag("promo_banner_text", "");

  if (!showBanner) return null;

  return <div className="promo">{message}</div>;
}

A plain client-only variant

If you are not on Next.js, fetch the config in the browser with useEffect and store it in state. The shape is identical: read data.values and hand it to your components.

// useConfig.ts (plain React, no Next.js)
import { useEffect, useState } from "react";

const ENDPOINT =
  "https://api-public.betterconfig.dev/v1/config?project=acme-web&env=prod";

export function useRemoteConfig() {
  const [config, setConfig] = useState<Record<string, unknown>>({});
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    let active = true;

    fetch(ENDPOINT, {
      // Browser-exposed token: must be env-scoped and read-only.
      headers: { Authorization: `Bearer ${import.meta.env.VITE_BC_READ_TOKEN}` },
    })
      .then((res) => (res.ok ? res.json() : { values: {} }))
      .then((data) => {
        if (active) setConfig(data.values ?? {});
      })
      .finally(() => {
        if (active) setLoading(false);
      });

    return () => {
      active = false;
    };
  }, []);

  return { config, loading };
}

This trades the server-side token handling for simplicity. Read the security note below before shipping a browser fetch.

Freshness: how changes propagate without a redeploy

This is the payoff. Because the read API is edge-cached (max-age=30, stale-while-revalidate=300) and Next's data cache is set to revalidate: 30, publishing a new value in BetterConfig propagates to your running app within the cache window. No rebuild, no redeploy, no restart. You change the value in the dashboard, and the app picks it up on the next revalidation.

You have two ways to control freshness in Next.js:

  • Time-based revalidation. next: { revalidate: 30 } refreshes the cached config every 30 seconds. Lower it for faster propagation, raise it to cut read volume.
  • On-demand revalidation. Call revalidatePath or revalidateTag from a route handler to flush the cache the instant you publish, instead of waiting for the window to elapse.

For most apps, time-based revalidation is enough: a 30-second worst case for a config change is dramatically faster than a deploy.

Security: treat the token like what it is

Read tokens are created in a project's Settings under API tokens. They are read-only. A project-scoped token can read any environment; an env-scoped token is locked to a single environment.

  • Prefer the server. Fetch config in a Server Component or route handler and pass values to the client. The token stays in an environment variable and never reaches the browser.
  • If you must fetch from the browser, use an env-scoped token and treat every value you publish to that environment as public. A read token in client code is visible to anyone who opens the network tab, so never put anything sensitive behind it.

The token grants read access to published config only. It cannot write, publish, or touch any other environment outside its scope.

Wrap up

With config out of your bundle and a small Context layer in front of it, the values that used to require a release become a dashboard edit. Toggle a feature, swap a banner, raise a limit, or pull a kill switch, and the change reaches your users within the cache window. Your deploys go back to being about code.