React Adapter
@useoptimus/react provides <FlagProvider>, useFlag, and useVariant.
Install
Section titled “Install”npm install @useoptimus/reactPeer dependency: react@^18.0.0 || ^19.0.0 — the floor is 18 because
useSyncExternalStore, which useFlag is built on, doesn’t exist in 17.
import { FlagProvider, useFlag } from "@useoptimus/react";
function App({ client, snapshot }) { return ( <FlagProvider client={client} snapshot={snapshot}> <Banner /> </FlagProvider> );}
function Banner() { const flag = useFlag<boolean>("show-banner"); return flag.value ? <div>New banner!</div> : null;}client is a FlagsClient from @useoptimus/core,
constructed and owned entirely by your app — <FlagProvider> never calls
client.init()/client.dispose(). React StrictMode’s double-effect-invoke
in dev would break a shared client on the second mount if it did, and a
client is typically one-per-app anyway, not scoped to a component subtree.
Snapshot vs. live mode
Section titled “Snapshot vs. live mode”Passing snapshot (a SerializedSnapshot from
@useoptimus/node) puts the provider in
snapshot mode: useFlag reads only from the hydrated snapshot, with
zero calls to client.evaluate()/evaluateAll() — the concrete expression
of the SSR contract: the server evaluates
once, the client hydrates without re-evaluating.
The only way out of snapshot mode is an explicit, field-different context
prop change:
<FlagProvider client={client} snapshot={snapshot} context={{ userId }}>A re-render with a new object, same fields does not flip to live mode
(an inline context={{}} literal is safe); a re-render with an actually
different field does, calling client.setContext() once and switching every
subsequent useFlag read to live client.evaluate() calls, subscribed to
live updates via client.subscribe().
useVariant
Section titled “useVariant”const variantKey = useVariant("checkout-v2"); // string | undefinedReturns undefined — never throws, never casts .value to a string — when
the flag has no variantKey (e.g. it fell through to its default with no
bucketing key resolvable). That’s a normal state, not a bug.
Testing
Section titled “Testing”pnpm --filter @useoptimus/react test