Persistent, cross-device user identifier for React Native/Expo. Port of StableID (Swift) to the Expo ecosystem, big thanks to him for that awesome lib!
expo-stable-id provides a persistent user identifier using dual storage:
- Cloud:
@nauverse/expo-cloud-settings(iCloud KVS on iOS, future Android support) - syncs across devices - Local:
expo-secure-store(Keychain on iOS, Android Keystore) - persists across app reinstalls
The ID is generated once and persisted to both storages. On subsequent launches, the stored ID is read back. When iCloud syncs a new ID from another device, the local copy is updated.
npx expo install @nauverse/expo-stable-id @nauverse/expo-cloud-settings expo-secure-store expo-cryptoAdd to your app.config.ts / app.json:
export default {
plugins: ['@nauverse/expo-stable-id'],
};This adds the iCloud KVS entitlement required for cloud sync. Optionally pass a custom container:
export default {
plugins: [
['@nauverse/expo-stable-id', { containerIdentifier: 'com.example.shared' }],
],
};Note:
StableIdProviderdoes not requireCloudSettingsProviderfrom@nauverse/expo-cloud-settingsas an ancestor. Internally it uses the functional API (getString,setString,addChangeListener) from@nauverse/expo-cloud-settingsdirectly. If your app also usesCloudSettingsProviderfor its own React hooks (useCloudSetting*), both providers are independent and can be placed in any order.
import { StableIdProvider, useStableId } from '@nauverse/expo-stable-id';
function App() {
return (
<StableIdProvider>
<MyComponent />
</StableIdProvider>
);
}
function MyComponent() {
const [id, { identify, generateNewId }] = useStableId();
return (
<View>
<Text>Stable ID: {id ?? 'Loading...'}</Text>
<Button title="New ID" onPress={() => generateNewId()} />
<Button title="Set Custom" onPress={() => identify('user-123')} />
</View>
);
}import { StandardGenerator, ShortIDGenerator } from '@nauverse/expo-stable-id';
// Use short 8-char IDs instead of UUIDs
<StableIdProvider config={{ generator: new ShortIDGenerator() }}>
// Provide a known ID, force it even if one exists
<StableIdProvider config={{ id: 'known-user-id', policy: 'forceUpdate' }}>
// Provide a fallback ID, prefer any stored value
<StableIdProvider config={{ id: 'fallback-id', policy: 'preferStored' }}>import {
configure,
getId,
identify,
generateNewId,
isConfigured,
hasStoredId,
addChangeListener,
setWillChangeHandler,
} from '@nauverse/expo-stable-id';
// Initialize (call once at app startup)
const id = await configure();
// Or with options
const id = await configure({
id: 'fallback-id',
policy: 'preferStored',
generator: new ShortIDGenerator(),
});
// Read current ID (sync, from cache)
const currentId = getId();
// Change ID
identify('new-user-id');
// Generate a new random ID
const newId = generateNewId();
// Check state
isConfigured(); // boolean
await hasStoredId(); // boolean
// Listen for changes
const subscription = addChangeListener((event) => {
console.log(`ID changed: ${event.previousId} -> ${event.newId} (${event.source})`);
});
subscription.remove();
// Intercept changes before they apply (identify, generateNewId, cloud sync)
setWillChangeHandler((currentId, candidateId) => {
// Return modified ID, or null to accept candidate as-is
return candidateId;
});Use the same stable ID across your analytics and payment provider so user events are always linked:
import { StableIdProvider, useStableId } from '@nauverse/expo-stable-id';
import PostHog from 'posthog-react-native';
import Purchases from 'react-native-purchases';
import { useEffect } from 'react';
function App() {
return (
<StableIdProvider>
<IdentifyProviders />
{/* rest of your app */}
</StableIdProvider>
);
}
function IdentifyProviders() {
const [id] = useStableId();
useEffect(() => {
if (!id) return;
// Same ID in both services
PostHog.identify(id);
Purchases.logIn(id);
}, [id]);
return null;
}| Generator | Output | Example |
|---|---|---|
StandardGenerator (default) |
UUID v4 | a1b2c3d4-e5f6-4789-abcd-ef0123456789 |
ShortIDGenerator |
8-char alphanumeric | xK9mP2nQ |
Custom generators implement the IDGenerator interface:
import type { IDGenerator } from '@nauverse/expo-stable-id';
const myGenerator: IDGenerator = {
generate: () => `prefix-${Date.now()}`,
};| Policy | Behavior |
|---|---|
'forceUpdate' (default) |
Always use the provided id (if given) |
'preferStored' |
Use stored ID if available, fall back to provided id |
| Feature | iOS | Android |
|---|---|---|
| Local storage (Keychain/Keystore) | Yes | Yes |
| Cloud sync (iCloud KVS) | Yes | Coming soon |
| ID generation | Yes | Yes |
| Function | Returns | Description |
|---|---|---|
configure(config?) |
Promise<string> |
Initialize and get/create stable ID |
getId() |
string | null |
Current cached ID (sync) |
identify(id) |
void |
Set a specific ID |
generateNewId() |
string |
Generate and persist a new ID |
isConfigured() |
boolean |
Whether configure() has been called |
hasStoredId() |
Promise<boolean> |
Whether an ID exists in storage |
addChangeListener(cb) |
{ remove: () => void } |
Subscribe to ID changes |
setWillChangeHandler(fn) |
void |
Intercept all ID changes before they apply |
| Export | Description |
|---|---|
StableIdProvider |
Context provider, call configure() internally |
useStableId() |
[id, { identify, generateNewId }] |
type IDPolicy = 'preferStored' | 'forceUpdate';
type ChangeSource = 'cloud' | 'manual';
interface IDGenerator {
generate(): string;
}
interface StableIdConfig {
readonly id?: string;
readonly generator?: IDGenerator;
readonly policy?: IDPolicy;
}
interface StableIdChangeEvent {
readonly previousId: string | null;
readonly newId: string;
readonly source: ChangeSource;
}| StableID (Swift) | expo-stable-id | Notes |
|---|---|---|
StableID.configure(id?, generator, policy) |
configure(config?) |
Same semantics |
StableID.id |
getId() / useStableId()[0] |
Sync cached read |
StableID.identify(id:) |
identify(id) |
Writes both storages |
StableID.generateNewID() |
generateNewId() |
Uses configured generator |
StableID.isConfigured |
isConfigured() |
Static check |
StableID.hasStoredID |
hasStoredId() |
Checks both storages |
StableID.set(delegate:) |
addChangeListener() + setWillChangeHandler() |
JS-idiomatic |
StandardGenerator |
StandardGenerator |
UUID v4 |
ShortIDGenerator |
ShortIDGenerator |
8-char alphanumeric |
MIT