React
The provider, the hooks, and the options worth knowing about.
Setting up the client
Create the client once. It owns the fetch schedule, the disk cache, and the telemetry buffer, so creating it per render would restart all three. Module scope is the simplest way to get that; if you are server-rendering and want to pass an initialSnapshot, a useState initialiser is still once per mount.
import { createKlorClient } from '@klor/react'
export const klor = createKlorClient({
apiKey: 'klor_pub_…',
refreshInterval: 300_000,
})apiKeystringA public key on clients, a private key on servers. Required.
refreshIntervalnumberHow often to poll, in milliseconds. Default 300000 (5 minutes), minimum 30000. Set 0 to refresh only on mount and on foreground.
storageKlorStorage | falseWhere the last snapshot is cached. Defaults to localStorage in a browser and memory elsewhere. Pass false to disable persistence.
initialSnapshotSnapshotA snapshot fetched on the server, so hydration does not flash fallback values.
telemetry{ enabled?: boolean }Anonymous usage counters, on by default. Set enabled: false to send nothing.
onError(error) => voidCalled when a refresh fails. Klor keeps serving the snapshot it already has.
Provider
The context prop is who the current user is. Rules are matched against it, and changing it re-evaluates every flag immediately.
<KlorProvider
client={klor}
context={{
userId: user.id,
attributes: { platform: 'web', country: user.country, plan: user.plan },
}}
>
<App />
</KlorProvider>userId and deviceId are reserved and read from the top level. Everything else (including platform and appVersion) goes in attributes.
useFlag
Synchronous, never suspends, never throws. Returns your fallback until the first snapshot lands.
const newCheckout = useFlag('checkout_v2', false)
const maxItems = useFlag('max_basket_items', 10)
const promo = useFlag('promo', { code: '', percent: 0 })The fallback also fixes the type. Asking for a boolean and receiving a number from a mistyped flag gives you the fallback rather than the wrong type.
useFlagDetail
Same value, plus why it was chosen. Useful in a debug panel or a log line.
const { value, reason, ruleId } = useFlagDetail('checkout_v2', false)
// reason: 'rule' | 'default' | 'disabled' | 'unknownFlag' | 'notReady' | 'typeMismatch'useKlor
The state of the client itself.
const { isReady, isStale, lastSyncedAt, seq, refresh } = useKlor()isReadybooleanA snapshot is in hand, from cache or network.
isStalebooleanThe last refresh failed but a cached snapshot is still being served.
seqnumber | nullWhich published snapshot is live. Useful in support conversations.
refresh() => Promise<void>Fetch now, ignoring the schedule.
Devtools
A panel that lists every flag in the current snapshot with the value it is serving and why, and lets you force a value locally. It is the only way to exercise the “on” path of a flag without editing configuration other people are also reading.
import { KlorDevtools } from '@klor/react/devtools'
<KlorProvider client={klor} context={context}>
<App />
{process.env.NODE_ENV !== 'production' && <KlorDevtools />}
</KlorProvider>Imported from @klor/react/devtools, so it stays out of your production bundle unless you put it there. Overrides never leave the device, win over whatever is published, and are never counted as usage. An override whose type does not match the flag is ignored rather than served.
The panel renders DOM, so it is web only. React Native has the same overrides through client.setOverride(key, value) and client.clearOverrides().
Server rendering
Fetch a snapshot on the server and hand it to the client so the first paint is correct rather than a flash of fallbacks.
// server
const snapshot = await serverKlor.getSnapshot()
// client
const klor = createKlorClient({ apiKey: 'klor_pub_…', initialSnapshot: snapshot })Only pass a snapshot fetched with a public key to the browser. A private-key snapshot contains flags marked sensitive.