React Native
The same package as the web. Two options need wiring up, because Klor deliberately does not depend on react-native.
Using Expo? See feature flags in an Expo app for EAS build profiles and how flags relate to OTA updates.
Why there are two options
@klor/react has no React Native dependency, not AsyncStorage, not MMKV, not react-native itself. That is what lets one package serve both platforms without a native module or a second install. The cost is that you pass in the two things only your app knows how to provide: where to cache, and how to tell when the app comes back to the foreground.
Setup
import AsyncStorage from '@react-native-async-storage/async-storage'
import { AppState } from 'react-native'
import { createKlorClient } from '@klor/react'
export const klor = createKlorClient({
apiKey: 'klor_pub_…',
// Any object with getItem/setItem. AsyncStorage and MMKV both fit as-is.
storage: AsyncStorage,
// Config is refreshed whenever the app returns to the foreground, which is
// when a user is most likely to see something stale.
subscribeToForeground: (refresh) => {
const subscription = AppState.addEventListener('change', (state) => {
if (state === 'active') refresh()
})
return () => subscription.remove()
},
// And usage counters are flushed on the way out, which is when a buffer
// would otherwise be lost.
subscribeToBackground: (flush) => {
const subscription = AppState.addEventListener('change', (state) => {
if (state !== 'active') flush()
})
return () => subscription.remove()
},
})subscribeToBackground is easy to skip and costs you most of your telemetry if you do. Counters are buffered in memory and flushed on a timer; on the web the SDK also flushes when the page is hidden, but React Native has no such event, so an app backgrounded inside the flush interval loses what it counted. That is most sessions.
Without storage, Klor falls back to memory, so a cold start with no network serves your fallbacks instead of the last known config. On mobile that is the case you most want covered.
Context
Pass the platform and the running version so rules and update gating have something to match on.
import { Platform } from 'react-native'
import DeviceInfo from 'react-native-device-info'
<KlorProvider
client={klor}
context={{
userId: user.id,
attributes: {
platform: Platform.OS, // 'ios' | 'android'
appVersion: DeviceInfo.getVersion(),
osVersion: String(Platform.Version),
},
}}
>
<App />
</KlorProvider>Any source for the version works: expo-constants, a generated constant, or a native module. Klor only needs a string it can parse.
Update gating
Klor ships no UI. useVersionGate returns a verdict and the copy to show; the prompt is yours, so it looks like the rest of your app.
import { useVersionGate } from '@klor/react'
function UpdateGate({ children }) {
const gate = useVersionGate()
if (gate.status === 'forced') {
return (
<Blocking
message={gate.message}
onPress={() => Linking.openURL(gate.storeUrl)}
/>
)
}
return (
<>
{gate.status === 'optional' && <UpdateBanner message={gate.message} url={gate.storeUrl} />}
{children}
</>
)
}Platform and version come from the provider context by default. Pass them explicitly if you need to.
Offline behaviour
With storage wired up, the last snapshot survives a restart. A launch with no connection serves the config from the last successful sync, and a failed refresh never discards what is already in hand. useKlor().isStale tells you when that is happening.