Skip to content
⚠️ Beta: API may change before v1.0. Pin to ~0.x.0 to avoid breaking changes.

createPersister()

Creates a persister instance for saving and restoring flow state. Adds features like TTL, versioning, and validation on top of storage stores.

import { createPersister } from "@useflow/react";
function createPersister(options: PersisterOptions): FlowPersister

Configuration object for the persister:

type PersisterOptions = {
store: FlowStore; // Required: Storage backend
ttl?: number; // Optional: Time to live in ms
validate?: (state: PersistedFlowState) => boolean; // Optional: State validation
onSave?: (flowId: string, state: PersistedFlowState) => void; // Optional: Save callback
onRestore?: (flowId: string, state: PersistedFlowState) => void; // Optional: Restore callback
onError?: (error: Error) => void; // Optional: Error handler
}
import {
createLocalStorageStore,
createSessionStorageStore,
createMemoryStore
} from "@useflow/react";
// Browser localStorage
const localStore = createLocalStorageStore();
// Browser sessionStorage
const sessionStore = createSessionStorageStore();
// In-memory (for testing)
const memoryStore = createMemoryStore();
type FlowStore = {
get: (flowId: string, options?: FlowStoreOptions) =>
Promise<PersistedFlowState | null> | PersistedFlowState | null;
set: (flowId: string, state: PersistedFlowState, options?: FlowStoreOptions) =>
Promise<void> | void;
remove: (flowId: string, options?: FlowStoreOptions) =>
Promise<void> | void;
removeFlow?: (flowId: string) => Promise<void> | void;
removeAll?: () => Promise<void> | void;
}
type FlowStoreOptions = {
instanceId?: string;
variantId?: string;
}
import { createPersister, createLocalStorageStore } from "@useflow/react";
const persister = createPersister({
store: createLocalStorageStore()
});
// Use with Flow component
<Flow
flow={myFlow}
persister={persister}
initialContext={{}}
>
{({ renderStep }) => renderStep({ /* ... */ })}
</Flow>
const persister = createPersister({
store: createLocalStorageStore(),
ttl: 7 * 24 * 60 * 60 * 1000 // 7 days
});
// State older than 7 days will be ignored
const persister = createPersister({
store: createLocalStorageStore(),
validate: (state) => {
// Ensure required fields exist
return state?.stepId && state?.context?.userId;
}
});
// Invalid states will be ignored on restore
const persister = createPersister({
store: createLocalStorageStore(),
onSave: (flowId, state) => {
console.log(`Saved ${flowId} at step ${state.stepId}`);
},
onRestore: (flowId, state) => {
console.log(`Restored ${flowId} from step ${state.stepId}`);
},
onError: (error) => {
console.error('Persistence error:', error);
}
});

Create a custom store for any backend:

const apiStore: FlowStore = {
get: async (flowId, options) => {
const params = new URLSearchParams({
flowId,
...(options?.instanceId && { instanceId: options.instanceId }),
...(options?.variantId && { variantId: options.variantId })
});
const response = await fetch(`/api/flow-state?${params}`);
if (!response.ok) return null;
return response.json();
},
set: async (flowId, state, options) => {
await fetch('/api/flow-state', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ flowId, state, ...options })
});
},
remove: async (flowId, options) => {
const params = new URLSearchParams({
flowId,
...(options?.instanceId && { instanceId: options.instanceId }),
...(options?.variantId && { variantId: options.variantId })
});
await fetch(`/api/flow-state?${params}`, {
method: 'DELETE'
});
}
};
const persister = createPersister({ store: apiStore });
const indexedDBStore: FlowStore = {
get: async (flowId, options) => {
const db = await openDB();
const key = formatKey(flowId, options);
return db.get('flows', key);
},
set: async (flowId, state, options) => {
const db = await openDB();
const key = formatKey(flowId, options);
await db.put('flows', state, key);
},
remove: async (flowId, options) => {
const db = await openDB();
const key = formatKey(flowId, options);
await db.delete('flows', key);
}
};
// Helper to format keys consistently
function formatKey(flowId: string, options?: FlowStoreOptions) {
const parts = [flowId];
if (options?.variantId) parts.push(options.variantId);
if (options?.instanceId) parts.push(options.instanceId);
return parts.join(':');
}
const persister = createPersister({ store: indexedDBStore });

The returned persister object has these methods:

await persister.save(
flowId: string,
state: FlowState,
options?: {
version?: string;
instanceId?: string;
variantId?: string;
}
): Promise<PersistedFlowState | null>
await persister.restore(
flowId: string,
options?: {
version?: string;
migrate?: MigrateFunction;
instanceId?: string;
variantId?: string;
}
): Promise<PersistedFlowState | null>
await persister.remove(
flowId: string,
options?: {
instanceId?: string;
variantId?: string;
}
): Promise<void>
await persister.removeFlow(
flowId: string
): Promise<void>

Removes all instances of a specific flow (base + all instances).

await persister.removeAll(): Promise<void>

Removes all flows managed by this persister.

The store implementation determines the key format. For key-value stores (localStorage, sessionStorage), keys typically follow this pattern:

[flowId]:[variantId]:[instanceId]

Examples:

  • onboarding:: - Default (no variant or instance)
  • onboarding:v2: - With variant
  • onboarding::user123 - With instance
  • checkout:v2:session456 - All options

Different stores may use different formats. Check your store’s documentation for specifics.

<Flow
flow={myFlow}
persister={persister}
onPersistenceError={(error) => {
console.error('Storage failed:', error);
// Fallback or notify user
}}
>
{/* ... */}
</Flow>
// Remove specific flow state
await persister.remove('onboarding', { instanceId: 'user123' });
// Remove all instances of a flow
await persister.removeFlow?.('onboarding');
// Clear all persisted data
await persister.removeAll?.();
// Different persistence for each user
<Flow
flow={checkoutFlow}
persister={persister}
instanceId={userId} // Separate storage per user
>
{/* ... */}
</Flow>

Handle breaking changes in your flow:

const migration: MigrateFunction = (oldState, fromVersion) => {
if (fromVersion === "v1") {
// Migrate v1 → v2
return {
...oldState,
context: {
...oldState.context,
newField: 'default'
}
};
}
return oldState;
};
// Flow will use migration function on restore
<Flow
flow={myFlow}
persister={persister}
>
{/* ... */}
</Flow>