React SDK
A provider and hooks that re-render your components when a flag changes, with server rendering that never mismatches on hydration.
@featflags/react wraps the TypeScript SDK in a provider and hooks. Components re-render when a flag changes or the user does, and evaluation stays in the browser, so your users’ attributes never leave it. It needs React 18 or later.
Install
npm install @featflags/react
It brings @featflags/js with it, published on npm as @featflags/react.
Add the provider
Wrap your app once, with the environment’s client key (pk_…):
// src/main.tsx
import { FeatflagsProvider } from '@featflags/react'
import { createRoot } from 'react-dom/client'
import { App } from './App'
createRoot(document.getElementById('root')!).render(
<FeatflagsProvider
options={{
key: import.meta.env.VITE_FEATFLAGS_KEY, // pk_…
cdnUrl: 'https://flags.featflags.io',
eventsUrl: 'https://events.featflags.io',
}}
context={{ key: currentUser.id, plan: currentUser.plan }}
>
<App />
</FeatflagsProvider>,
)
| Prop | What it does |
|---|---|
options |
The TypeScript SDK’s client options. The provider starts a client on mount and closes it on unmount; later changes to options are ignored. |
client |
A FlagClient you created yourself, instead of options. The provider never closes it. |
context |
Who to evaluate for. Changing its value calls identify; a new object with the same contents doesn’t. |
membershipTokens |
Tokens for private segments for context, from your backend. |
experimentSeeds |
Experiment seeds for context, from your backend. |
Pass options or client, not both. A client created from options starts in an effect, because React may render a tree and throw it away (StrictMode, Suspense) and a started client polls. Until then the provider evaluates options.bootstrap, or your fallbacks, without fetching or counting anything. So useFeatflagsClient() returns the live client only once the provider has mounted: change the user through the context prop or from event handlers, not from a child’s mount effect.
Read flags
import { useFeatflagsClient, useFlag, useFlagDetail } from '@featflags/react'
export function Checkout() {
const newCheckout = useFlag('new-checkout', false)
const pricing = useFlagDetail('pricing-page', 'v1') // { value, variant, reason, ruleId, prerequisite }
const flags = useFeatflagsClient()
return newCheckout ? <NewCheckout pricing={pricing.value} onUpgrade={(plan) => flags.identify({ key: currentUser.id, plan })} /> : <OldCheckout />
}
useFlag(key, fallback) returns the flag’s value for the provider’s context, or fallback when the flag doesn’t exist, the flags haven’t loaded, or the value has another type than the fallback. A boolean fallback asks for a boolean, and so on; an array, object or null fallback accepts any value. useFlagDetail adds the variant and the reason.
Both evaluate once per change, not once per render, so re-renders don’t inflate your usage counts. Components re-render when newer flags load, when the context prop changes value, and when anything calls identify on the client, including once the new context’s list shards have loaded.
Server rendering
The server renders from the same flags the browser starts with, so the first client render matches and hydration never mismatches. Fetch the client-key flags on the server, render with them as bootstrap, and hand them to the browser:
// server.tsx
import { FeatflagsProvider } from '@featflags/react'
import { renderToString } from 'react-dom/server'
import { App } from './App'
const options = { key: 'pk_…', cdnUrl: 'https://flags.featflags.io', eventsUrl: 'https://events.featflags.io' }
export async function render(user: User) {
const context = { key: user.id, plan: user.plan }
// The CDN caches this; keep your own copy for a minute if you render a lot.
const bootstrap = await (await fetch(`${options.cdnUrl}/v1/${options.key}.json`)).json()
const html = renderToString(
<FeatflagsProvider options={{ ...options, bootstrap }} context={context}>
<App />
</FeatflagsProvider>,
)
const state = JSON.stringify({ bootstrap, context }).replace(/</g, '\\u003c')
return `<div id="root">${html}</div><script>window.__FLAGS__ = ${state}</script><script type="module" src="/client.js"></script>`
}
// client.tsx
import { FeatflagsProvider } from '@featflags/react'
import { hydrateRoot } from 'react-dom/client'
import { App } from './App'
const { bootstrap, context } = window.__FLAGS__
hydrateRoot(
document.getElementById('root')!,
<FeatflagsProvider options={{ key: 'pk_…', cdnUrl: 'https://flags.featflags.io', eventsUrl: 'https://events.featflags.io', bootstrap }} context={context}>
<App />
</FeatflagsProvider>,
)
Use the client key’s flags for this, never a server key’s: server keys’ flags carry the secrets of private segments. On the server the provider never starts a client: it evaluates the bootstrap and fetches, polls and counts nothing. The browser hydrates from the same bootstrap and context, then fetches, and re-renders if there are newer flags. Give the context a key on both sides: an anonymous key is made in the browser, so a context without one buckets differently on the server.
Experiments
useTrack() returns track(eventKey, value?) for the provider’s context. It reports what the user did to every running experiment whose metrics count eventKey, and sends nothing when none does:
import { useTrack } from '@featflags/react'
export function BuyButton({ total }: { total: number }) {
const track = useTrack()
return <button onClick={() => (track('checkout.completed', total), checkout())}>Buy</button>
}
The function it returns never changes and always reaches the live client, so a handler memoised on the first render still counts. Call it from event handlers, not a child’s mount effect, which runs before the live client starts.
Experiment IDs are computed with your environment’s experiment key. Keep it on your backend and pass each signed-in user’s seeds (from experimentSeeds(context) in a server SDK) as the provider’s experimentSeeds, with the context they belong to; a change of either identifies again. For anonymous visitors, pass experimentKey in options instead, knowing that a key in a browser bundle is public. The TypeScript SDK has the details.
Everything else
Hooks, OpenTelemetry, lists, browser origins and how changes arrive are the TypeScript SDK’s: pass them in options and see its page.