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:
// vite.config.ts
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [react()],
});// tsconfig.json
{
"compilerOptions": {
"jsx": "react-jsx",
},
}Setup
Create the client once (see the Quickstart) and provide it at the root:
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
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
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
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>whosenamematches the assignment renders; the others render nothing. fallbackrenders 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:
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:
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.