SkillAgentSearch skills...

Travels

A fast, framework-agnostic undo/redo core powered by Mutative JSON Patch

Install / Use

/learn @mutativejs/Travels
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Travels

Node CI npm license

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

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 changes
    • state: The new state
    • patches: The current patches history
    • `positio
View on GitHub
GitHub Stars841
CategoryDevelopment
Updated1d ago
Forks8

Languages

TypeScript

Security Score

100/100

Audited on Mar 25, 2026

No findings