Flags guides
09 · React and Next.js

Flags in the browser.

Hooks for React, server rendering in Next.js without a flash, and which key may ship to a browser.


1

Which key goes in the browser

Anything you put in a browser bundle is public, SDK keys included. A key can read its whole config: every flag key and every clear-text rule value. So create a separate client config (for example “Web”) for flags the browser needs, with no internal rules, and keep your server config's key on the server. Use hashed comparators for lists of users.


2

React

terminal
npm install @productcraft/flags @productcraft/flags-react
App.tsx
import { FlagsProvider, useFlag, useFlagDetails } from "@productcraft/flags-react";

export function App({ user }: { user: { id: string; plan: string } }) {
  return (
    <FlagsProvider
      options={{ sdkKey: import.meta.env.VITE_FLAGS_SDK_KEY }}
      user={{ identifier: user.id, custom: { plan: user.plan } }}
    >
      <Checkout />
    </FlagsProvider>
  );
}

function Checkout() {
  const newCheckout = useFlag("new_checkout", false); // boolean, from the default's type
  const limits = useFlag("limits", { max: 5 }); // a json setting, parsed
  const banner = useFlagDetails("banner_text", "Welcome"); // value, reason, rule, revision
  return newCheckout ? (
    <p>
      New checkout, up to {limits.max} items. {banner.value}
    </p>
  ) : (
    <p>Old checkout</p>
  );
}

useFlag takes its type from your default and re-renders when a new config arrives. In a browser the client relies on the HTTP cache to revalidate (no CORS preflight per poll) and pauses polling while the tab is hidden.


3

Next.js App Router, no flash

Evaluate on the server with a client that holds the same config as the browser (the client config's key) and pass its snapshot as bootstrap. The server render and the first browser render then agree, so there is no flash and no hydration mismatch; the browser's own download starts after mount.

app/layout.tsx
// app/layout.tsx (a server component)
import type { ReactNode } from "react";
import { FlagsClient } from "@productcraft/flags";
import { FlagsProvider } from "@productcraft/flags-react";
import { currentUser } from "@/lib/auth";

// One client per server process, with the CLIENT config's key: the
// same config the browser downloads, so server and browser agree.
const serverFlags = new FlagsClient({ sdkKey: process.env.NEXT_PUBLIC_FLAGS_SDK_KEY! });

export default async function RootLayout({ children }: { children: ReactNode }) {
  await serverFlags.waitForReady();
  const user = await currentUser();
  return (
    <html lang="en">
      <body>
        <FlagsProvider
          options={{ sdkKey: process.env.NEXT_PUBLIC_FLAGS_SDK_KEY! }}
          bootstrap={serverFlags.snapshot().toJSON() ?? undefined}
          user={{ identifier: user.id }}
        >
          {children}
        </FlagsProvider>
      </body>
    </html>
  );
}

A server component that only needs a value can read it with the server config's key, which never reaches the browser:

app/pricing/page.tsx
// app/pricing/page.tsx (a server component)
import { FlagsClient } from "@productcraft/flags";
import { currentUser } from "@/lib/auth";

// A server config's key: this one never reaches the browser.
const flags = new FlagsClient({ sdkKey: process.env.PRODUCTCRAFT_FLAGS_SDK_KEY });

export default async function PricingPage() {
  const user = await currentUser();
  const annualFirst = await flags.getValue("pricing_annual_first", false, { identifier: user.id });
  return <h1>{annualFirst ? "Annual plans" : "Monthly plans"}</h1>;
}

4

Without React

main.ts
import { FlagsClient } from "@productcraft/flags";

// A returning visitor starts from the last config they saw.
const store = {
  get: (k: string) => localStorage.getItem(k),
  set: (k: string, v: string) => localStorage.setItem(k, v),
};

const flags = new FlagsClient({
  sdkKey: "pcf_sdk_…", // a CLIENT config's key: it is public once it ships
  cache: store,
  defaultUser: { identifier: "user-123" },
});
flags.on("configChanged", () => render());
const showBanner = await flags.getValue("show_banner", false);

declare function render(): void;
console.log(showBanner);