Skip to content

React

Everything React lives under the /react subpath. React (>=18) is an optional peer — the SDK's root entry never imports it.

Vite

The usual React transform is all you need — the SDK ships no JSX of its own (plain createElement), so it works with any bundler without special handling. The plugin is for your app's JSX and fast refresh:

ts
// vite.config.ts
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [react()],
});
jsonc
// tsconfig.json
{
  "compilerOptions": {
    "jsx": "react-jsx",
  },
}

Setup

Create the client once (see the Quickstart) and provide it at the root:

tsx
import { GatesProvider } from "@lucerna-dev/gates-browser/react";
import { gates } from "./lib/gates";

export function App() {
  return (
    <GatesProvider client={gates}>
      <Routes />
    </GatesProvider>
  );
}

Everything below re-renders automatically when decisions change — first load, identity switch, refresh().

<Feature> — flag gating

tsx
import { Feature } from "@lucerna-dev/gates-browser/react";

<Feature name="new_billing" fallback={<OldBilling />}>
  <NewBilling />
</Feature>;

Children render while the flag is on; fallback (default: nothing) while it's off or decisions haven't loaded.

<KillSwitch> — emergency cutoff

tsx
import { KillSwitch } from "@lucerna-dev/gates-browser/react";

<KillSwitch name="payments" fallback={<PaymentsDown />}>
  <Payments />
</KillSwitch>;

Children render while the guarded path is alive; fallback while the switch is thrown. Unknown switches are alive — a kill switch fails open, never off.

<Experiment> / <Variant> — declarative variants

tsx
import { Experiment, Variant } from "@lucerna-dev/gates-browser/react";

<Experiment name="checkout_test" fallback={<Steps />}>
  <Variant name="control">
    <Steps />
  </Variant>
  <Variant name="one_page">
    <OnePage />
  </Variant>
</Experiment>;
  • The <Variant> whose name matches the assignment renders; the others render nothing.
  • fallback renders when the user isn't in the experiment or the assigned variant has no matching <Variant> child (an old bundle running against a newer experiment) — nothing ever renders blank.
  • <Variant> elements must be direct children of <Experiment>.

Hooks

For anything the components don't cover:

tsx
import { useExperiment, useFlag, useGates, useKillSwitch } from "@lucerna-dev/gates-browser/react";

function Checkout() {
  const variant = useExperiment("checkout_test"); // "one_page" | null
  const billing = useFlag("new_billing"); // false until loaded
  const paymentsAlive = useKillSwitch("payments"); // false = killed
  const gates = useGates(); // the client, for refresh()
  // …
}

useGates() throws when there's no <GatesProvider> above — that's a wiring bug, not a runtime condition.

Server-side rendering

The bindings are SSR-safe: on the server there are no decisions yet, so rendered output takes the fail-open branch (fallback, flags off) and corrects itself after the client bootstraps. If first-paint flicker matters, gate above-the-fold surfaces server-side with the Node SDK, or pass storage so returning visitors hydrate from cached decisions.

Testing

Inject the transport — no network, no mocking library:

tsx
const gates = createGates({
  clientKey: "ck_client_test",
  fetch: () =>
    Promise.resolve({
      status: 200,
      headers: { get: () => null },
      json: () =>
        Promise.resolve({
          environment: "test",
          kills: {},
          flags: { new_billing: { on: true, reason: "rollout" } },
          experiments: {},
        }),
    }),
});

Render under <GatesProvider client={gates}> and await gates.ready().

UX only

Browser decisions show and hide UI. Authorization lives on your backend — anything in the browser can be tampered with.

Lucerna Developer Docs