Travels
A fast, framework-agnostic undo/redo core powered by Mutative JSON Patch
Install / Use
/learn @mutativejs/TravelsREADME
Travels
A fast, framework-agnostic undo/redo library that stores only changes, not full snapshots.
Travels gives your users the power to undo and redo their actions—essential for text editors, drawing apps, form builders, and any interactive application. Unlike traditional undo systems that copy entire state objects for each change, Travels stores only the differences (JSON Patches), making it 10x faster and far more memory-efficient.
Works with React, Vue, Zustand, or vanilla JavaScript.
Table of Contents
- Why Travels? Performance That Scales
- Installation
- Quick Start
- Core Concepts
- API Reference
- Mutable Mode: Keep Reactive State In Place
- Archive Mode: Control When Changes Are Saved
- State Requirements: JSON-Serializable Only
- Framework Integration
- Persistence: Saving History to Storage
- TypeScript Support
- Advanced: Extending Travels with Custom Logic
- Related Projects
- License
Why Travels? Performance That Scales
Traditional undo systems clone your entire state object for each change. If your state is 1MB and the user makes 100 edits, that's 100MB of memory. Travels stores only the differences between states (JSON Patches following RFC 6902), so that same 1MB object with 100 small edits might use just a few kilobytes.
Two key advantages:
-
Memory-efficient history storage - Stores only differences (patches), not full snapshots. Changing one field in a large object stores only a few bytes.
-
Fast immutable updates - Built on Mutative, which is 10x faster than Immer. Write simple mutation code like
draft.count++while maintaining immutability.
Framework-agnostic - Works with React, Vue, Zustand, MobX, Pinia, or vanilla JavaScript.
Installation
npm install travels mutative
# or
yarn add travels mutative
# or
pnpm add travels mutative
Integrations
- Zustand: zustand-travel - A powerful and high-performance time-travel middleware for Zustand
- React: use-travel - A React hook for state time travel with undo, redo, reset and archive functionalities.
Quick Start
import { createTravels } from 'travels';
// Create a travels instance with initial state
const travels = createTravels({ count: 0 });
// Subscribe to state changes
const unsubscribe = travels.subscribe((state, patches, position) => {
console.log('State:', state);
console.log('Position:', position);
});
// Update state using mutation syntax (preferred - more intuitive)
travels.setState((draft) => {
draft.count += 1; // Mutate the draft directly
});
// Or set state directly by providing a new value
travels.setState({ count: 2 });
// Undo the last change
travels.back();
// Redo the undone change
travels.forward();
// Get current state
console.log(travels.getState()); // { count: 1 }
// Cleanup when done
unsubscribe();
Try it yourself: Travels Counter Demo
⚠️ Important: State Requirements
Your state must be JSON-serializable (plain objects, arrays, strings, numbers, booleans, null) or Map/Set(Supported only in immutable mode; not supported in mutable mode.). Complex types like Date, class instances, and functions are not supported and may cause unexpected behavior. See State Requirements for details.
Core Concepts
Before diving into the API, understanding these terms will help:
State - Your application data. In the example above, { count: 0 } is the state.
Draft - A temporary mutable copy of your state that you can change freely. When you use setState((draft) => { draft.count++ }), the draft parameter is what you modify. Travels converts your mutations into immutable updates automatically.
Patches - The differences between states, stored as JSON Patch operations. Instead of saving entire state copies, Travels saves these small change records to minimize memory usage.
Position - Your current location in the history timeline. Position 0 is the initial state, position 1 is after the first change, etc. Moving back decreases position; moving forward increases it.
Archive - The act of saving the current state to history. By default, every setState call archives automatically. You can disable this and control archiving manually for more advanced use cases.
API Reference
createTravels(initialState, options?)
Creates a new Travels instance.
Parameters:
| Parameter | Type | Description | Default |
| ------------------ | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- |
| initialState | S | Your application's starting state (must be JSON-serializable) | (required) |
| maxHistory | number | Maximum number of history entries to keep. Older entries are dropped. Must be a non-negative integer (NaN, Infinity, decimals are rejected). | 10 |
| initialPatches | TravelPatches | Restore saved patches when loading from storage | {patches: [],inversePatches: []} |
| strictInitialPatches | boolean | Whether invalid initialPatches should throw. When false, invalid patches are discarded and history starts empty | false |
| initialPosition | number | Restore position when loading from storage | 0 |
| autoArchive | boolean | Automatically save each change to history (see Archive Mode) | true |
| mutable | boolean | Whether to mutate the state in place (for observable state like MobX, Vue, Pinia) | false |
| patchesOptions | boolean | PatchesOptions | Customize JSON Patch format. Supports { pathAsArray: boolean } to control path format. See Mutative patches docs | true (enable patches) |
| enableAutoFreeze | boolean | Prevent accidental state mutations outside setState (learn more) | false |
| strict | boolean | Enable stricter immutability checks (learn more) | false |
| mark | Mark<O, F>[] | Mark certain objects as immutable (learn more) | () => void |
Returns: Travels<S, F, A> - A Travels instance
Instance Methods
getState(): S
Get the current state.
setState(updater: S | (() => S) | ((draft: Draft<S>) => void)): void
Update the state. Supports three styles:
- Direct value:
setState({ count: 1 })- Replace state with a new object - Function returning value:
setState(() => ({ count: 1 }))- Compute new state - Draft mutation (recommended):
setState((draft) => { draft.count = 1 })- Mutate a draft copy
Performance Optimization: Updates that produce no actual changes (empty patches) won't create history entries or trigger subscribers. For example,
setState(state => state)or conditional updates that don't modify any fields. This prevents memory bloat from no-op operations.
subscribe(listener: (state, patches, position) => void): () => void
Subscribe to state changes. Returns an unsubscribe function.
Parameters:
listener: Callback function called on state changesstate: The new statepatches: The current patches history- `positio
