Skip to content

React

Use Gates in a React SPA through OpenFeature's client stack: the Lucerna web provider (@lucerna-dev/gates-openfeature-web) plugs into @openfeature/web-sdk, and @openfeature/react-sdk layers hooks over it — no Lucerna-specific React package needed.

The provider follows OpenFeature's static-context paradigm, which is exactly how the Browser SDK underneath works: one identity-resolved decisions snapshot is fetched with the environment's publishable client key, every read answers synchronously from it, and a context change refetches. Targeting rules never reach the browser — only decisions do.

1. Get your client key

In the dashboard, open Settings → API keys and copy the client key (ck_client_…) — publishable, safe to embed, and it pins the environment. Secret keys (ck_srv_…, ck_key_…) are rejected at construction: a secret in a browser bundle is readable by anyone.

2. Install

bash
pnpm add @lucerna-dev/gates-openfeature-web @lucerna-dev/gates-browser @openfeature/web-sdk @openfeature/react-sdk
bash
npm install @lucerna-dev/gates-openfeature-web @lucerna-dev/gates-browser @openfeature/web-sdk @openfeature/react-sdk
bash
bun add @lucerna-dev/gates-openfeature-web @lucerna-dev/gates-browser @openfeature/web-sdk @openfeature/react-sdk

@lucerna-dev/gates-browser and @openfeature/web-sdk are peer dependencies of the provider.

3. Register the provider

Once, at startup. Set the context first so the initial snapshot is already targeted:

ts
import { OpenFeature } from "@openfeature/web-sdk";
import { LucernaWebProvider } from "@lucerna-dev/gates-openfeature-web";

await OpenFeature.setContext({ targetingKey: user.id, plan: user.plan });
OpenFeature.setProvider(
  new LucernaWebProvider({
    clientKey: import.meta.env.VITE_LUCERNA_CLIENT_KEY,
    onError: (error) => console.error("[gates]", error.message),
  }),
);

targetingKey becomes the Gates userId (the sticky-bucketing unit); other attributes become traits your targeting rules match on — the same context mapping as the server provider. Wire onError: reads never throw, so without it a misconfigured key serves safe defaults ("everything off") silently.

4. Read flags in components

Wrap the tree once, then use hooks — every read is synchronous from the snapshot:

tsx
import {
  OpenFeatureProvider,
  useBooleanFlagValue,
  useStringFlagValue,
} from "@openfeature/react-sdk";

function App() {
  return (
    <OpenFeatureProvider>
      <Billing />
    </OpenFeatureProvider>
  );
}

function Billing() {
  const newBilling = useBooleanFlagValue("new_billing", false); // flag → boolean
  const variant = useStringFlagValue("checkout_test", "control"); // experiment → variant name
  return newBilling ? <NewBilling variant={variant} /> : <ClassicBilling />;
}

Components re-render automatically when decisions change — a context switch, or a refetch. Boolean reads answer flags with kill switches folded in (a thrown switch reads false); string reads answer experiment variants.

Prefer no flash of defaults while the first snapshot loads? <OpenFeatureProvider suspendUntilReady> suspends children until the provider is ready — pair it with a <Suspense> boundary. The react-sdk also ships a declarative <FeatureFlag> component and details/suspense hook variants.

5. React to login and logout

Identity lives in the global context. When the user changes, set it again — the provider refetches decisions for the new identity and components re-render when the swap completes:

ts
// login
await OpenFeature.setContext({ targetingKey: user.id, plan: user.plan });

// logout — back to an anonymous context
await OpenFeature.setContext({});

Using @lucerna-dev/identity? Feed it into OpenFeature one-directionally and every identify(), trait() and reset() flows into a refetch:

ts
identity.onChange((user) => {
  void OpenFeature.setContext({ targetingKey: user.userId, ...user.traits });
});

Options

ts
new LucernaWebProvider(options);
OptionDefaultWhat it does
clientKeyrequired; the environment's publishable client key (ck_client_…)
baseUrlhttps://api.uselucerna.appoverride for self-hosted or local development
requestTimeoutMs5000per-request timeout
readyTimeoutMs10000cap on how long initialize waits for the first snapshot before coming up on safe defaults
onErrortap for bootstrap/refresh failures — resolvers never throw
fetchplatform fetchoverride the transport (tests, custom dispatchers)
storageoptional decisions cache (e.g. localStorage) so the next page load reads instantly
storageKeylucerna:gatescache key when storage is set

Failure semantics

Resolvers never throw. Before the first snapshot — or when bootstrap fails — boolean reads answer safe false and string reads answer your default, while the error surfaces through onError. A rejected key (401/403) fails initialization loudly; a slow or offline network never blocks the app — the provider comes up on safe defaults at readyTimeoutMs and recovers on the next context change.

Decisions are UX hints

Decisions sent to a browser can be tampered with. Use them to show or hide UI — always re-check permissions on your backend.

Next

Lucerna Developer Docs