Migration Guide
July 18, 2026 ยท View on GitHub
From Redux-undo
Redux-undo stores whole state snapshots. Travels stores JSON Patch entries.
- Keep your existing reducer or state updater as the source of state changes.
- Create a Travels instance with the reducer state.
- Replace
UNDO/REDOdispatches withtravels.back()/travels.forward(). - Use
metadatalabels for undo menu text. - Persist with
travels.serialize()instead of storing the whole Redux-undo history object.
Snapshot stacks can still be better for small state and short local-only history. Travels is strongest when serialized history size matters.
From Zundo
Zundo integrates directly with Zustand stores. Travels can be used as a separate history engine:
- Keep Zustand as the UI store.
- Call
travels.setState(...)inside store actions. - Sync
travels.getState()back into Zustand after state changes. - Use
Travels.deserialize(...)and thehistoryoption for persisted history.
See examples/zustand.ts.
From use-travel
use-travel is a React hook wrapper. Use Travels directly when you need framework-agnostic control, persistence adapters, metadata, or custom store integration.
- Create
const travels = createTravels(initialState). - Subscribe with
useSyncExternalStore. - Expose
travels.getControls()to components. - Use
serialize()/Travels.deserialize()for reloads.
See examples/react-integration.tsx.
From Positional Subscribe Callbacks
Subscribers now receive the same TravelsEvent object as the devtools hook.
Replace positional callback parameters with event destructuring:
// Before
travels.subscribe((state, patches, position, historyLength) => {
render(state, position);
});
// After
travels.subscribe(
({ type, state, patches, position, historyLength, metadata }) => {
render(state, position);
}
);
One shallow-frozen event object is shared by all observers for a notification.
Its patches property remains an event-local delta, is materialized lazily,
and must be treated as read-only. Call travels.getPatches() when full retained
history is required. TravelsDevtoolsEvent remains as a deprecated type alias
for TravelsEvent. Callbacks that ignore their arguments, such as external store
invalidation functions, can remain unchanged.
From Map/Set State
Travels no longer supports Map or Set in either immutable or mutable state. Initial state and updates fail fast, and restored state or patch payloads containing collections are rejected during structural validation. Normalize collections before creating or updating a Travels instance:
type Item = { id: string; title: string };
type LegacyState = {
itemsById: Map<string, Item>;
selectedIds: Set<string>;
};
type TravelsState = {
itemsById: Record<string, Item>;
selectedIds: string[];
};
declare const legacyState: LegacyState;
const normalizeState = (state: LegacyState): TravelsState => ({
itemsById: Object.fromEntries(state.itemsById),
selectedIds: Array.from(state.selectedIds),
});
const travels = createTravels(normalizeState(legacyState));
Use stable string IDs before converting Maps with non-string or object keys. Keep any Map/Set view required by the application outside Travels and derive it from the normalized state.
Do not replay old patch history generated from Map/Set mutations. Such history can contain collection-specific values or non-JSON path locators whose reference identity cannot be migrated reliably. Decode the legacy record outside Travels, materialize its authoritative current state, normalize that state, and create a new history baseline. A persistence codec may convert between application-domain collections and storage, but its output passed to Travels must remain in the supported JSON-shaped contract.
Persistence Migration
Older hand-rolled persistence usually saved { state, patches, position }. Convert it with migrate:
Migration and function-valued fallback callbacks must return synchronously.
Complete asynchronous storage or network work before calling
Travels.deserialize(...); Promise-like callback results are rejected as
MIGRATION_FAILED or FALLBACK_FAILED without leaking an unhandled rejection.
type CurrentSnapshot = TravelsSerializedHistory<TravelsState>;
const history = Travels.deserialize<TravelsState>(stored, {
validation: 'semantic',
migrate(snapshot) {
if (snapshot && typeof snapshot === 'object' && !('version' in snapshot)) {
const legacy = snapshot as Omit<CurrentSnapshot, 'version'>;
return {
version: 1,
state: legacy.state,
patches: legacy.patches,
position: legacy.position,
};
}
return snapshot as CurrentSnapshot;
},
});