{"id":35851661,"url":"https://github.com/mutativejs/travels","last_synced_at":"2026-01-23T19:08:46.587Z","repository":{"id":317567504,"uuid":"1067001687","full_name":"mutativejs/travels","owner":"mutativejs","description":"A fast, framework-agnostic undo/redo core powered by Mutative JSON Patch","archived":false,"fork":false,"pushed_at":"2025-11-15T07:17:01.000Z","size":359,"stargazers_count":328,"open_issues_count":0,"forks_count":4,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-01-22T01:53:24.390Z","etag":null,"topics":["redo","time-tracking","undo","undo-redo"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mutativejs.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-09-30T08:48:36.000Z","updated_at":"2026-01-20T00:26:42.000Z","dependencies_parsed_at":null,"dependency_job_id":"feeb1b8b-80b8-4f77-b3f7-c044c3a4f075","html_url":"https://github.com/mutativejs/travels","commit_stats":null,"previous_names":["mutativejs/travels"],"tags_count":14,"template":false,"template_full_name":null,"purl":"pkg:github/mutativejs/travels","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mutativejs%2Ftravels","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mutativejs%2Ftravels/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mutativejs%2Ftravels/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mutativejs%2Ftravels/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mutativejs","download_url":"https://codeload.github.com/mutativejs/travels/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mutativejs%2Ftravels/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28698388,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-23T17:25:48.045Z","status":"ssl_error","status_checked_at":"2026-01-23T17:25:47.153Z","response_time":59,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["redo","time-tracking","undo","undo-redo"],"created_at":"2026-01-08T08:02:10.011Z","updated_at":"2026-01-23T19:08:46.580Z","avatar_url":"https://github.com/mutativejs.png","language":"TypeScript","readme":"# Travels\n\n![Node CI](https://github.com/mutativejs/travels/workflows/Node%20CI/badge.svg)\n[![npm](https://img.shields.io/npm/v/travels.svg)](https://www.npmjs.com/package/travels)\n![license](https://img.shields.io/npm/l/travels)\n\n**A fast, framework-agnostic undo/redo library that stores only changes, not full snapshots.**\n\nTravels 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**.\n\nWorks with React, Vue, Zustand, or vanilla JavaScript.\n\n## Table of Contents\n\n- [Why Travels? Performance That Scales](#why-travels-performance-that-scales)\n- [Installation](#installation)\n- [Quick Start](#quick-start)\n- [Core Concepts](#core-concepts)\n- [API Reference](#api-reference)\n  - [createTravels](#createtravelsinitialstate-options)\n  - [Instance Methods](#instance-methods)\n  - [maxHistory option](#maxhistory-option)\n- [Mutable Mode: Keep Reactive State In Place](#mutable-mode-keep-reactive-state-in-place)\n- [Archive Mode: Control When Changes Are Saved](#archive-mode-control-when-changes-are-saved)\n- [State Requirements: JSON-Serializable Only](#state-requirements-json-serializable-only)\n- [Framework Integration](#framework-integration)\n- [Persistence: Saving History to Storage](#persistence-saving-history-to-storage)\n- [TypeScript Support](#typescript-support)\n- [Advanced: Extending Travels with Custom Logic](#advanced-extending-travels-with-custom-logic)\n- [Related Projects](#related-projects)\n- [License](#license)\n\n## Why Travels? Performance That Scales\n\nTraditional 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](https://jsonpatch.com/)), so that same 1MB object with 100 small edits might use just a few kilobytes.\n\n**Two key advantages:**\n\n- **Memory-efficient history storage** - Stores only differences (patches), not full snapshots. Changing one field in a large object stores only a few bytes.\n\n- **Fast immutable updates** - Built on [Mutative](https://github.com/unadlib/mutative), which is [10x faster than Immer](https://mutative.js.org/docs/getting-started/performance). Write simple mutation code like `draft.count++` while maintaining immutability.\n\n**Framework-agnostic** - Works with React, Vue, Zustand, MobX, Pinia, or vanilla JavaScript.\n\n## Installation\n\n```bash\nnpm install travels mutative\n# or\nyarn add travels mutative\n# or\npnpm add travels mutative\n```\n\n#### Integrations\n\n- Zustand: [zustand-travel](https://github.com/mutativejs/zustand-travel) - A powerful and high-performance time-travel middleware for Zustand\n- React: [use-travel](https://github.com/mutativejs/use-travel) - A React hook for state time travel with undo, redo, reset and archive functionalities.\n\n## Quick Start\n\n```typescript\nimport { createTravels } from 'travels';\n\n// Create a travels instance with initial state\nconst travels = createTravels({ count: 0 });\n\n// Subscribe to state changes\nconst unsubscribe = travels.subscribe((state, patches, position) =\u003e {\n  console.log('State:', state);\n  console.log('Position:', position);\n});\n\n// Update state using mutation syntax (preferred - more intuitive)\ntravels.setState((draft) =\u003e {\n  draft.count += 1; // Mutate the draft directly\n});\n\n// Or set state directly by providing a new value\ntravels.setState({ count: 2 });\n\n// Undo the last change\ntravels.back();\n\n// Redo the undone change\ntravels.forward();\n\n// Get current state\nconsole.log(travels.getState()); // { count: 1 }\n\n// Cleanup when done\nunsubscribe();\n```\n\n**Try it yourself:** [Travels Counter Demo](https://codesandbox.io/p/sandbox/travels-vanilla-ts-wzdd62)\n\n---\n\n**⚠️ Important: State Requirements**\n\nYour 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](#state-requirements-json-serializable-only) for details.\n\n---\n\n## Core Concepts\n\nBefore diving into the API, understanding these terms will help:\n\n**State** - Your application data. In the example above, `{ count: 0 }` is the state.\n\n**Draft** - A temporary mutable copy of your state that you can change freely. When you use `setState((draft) =\u003e { draft.count++ })`, the `draft` parameter is what you modify. Travels converts your mutations into immutable updates automatically.\n\n**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.\n\n**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.\n\n**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.\n\n## API Reference\n\n### `createTravels(initialState, options?)`\n\nCreates a new Travels instance.\n\n**Parameters:**\n\n| Parameter          | Type                      | Description                                                                                                                                                                     | Default                          |\n| ------------------ | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- |\n| `initialState`     | S                         | Your application's starting state (must be [JSON-serializable](#state-requirements-json-serializable-only))                                                                     | (required)                       |\n| `maxHistory`       | number                    | Maximum number of history entries to keep. Older entries are dropped.                                                                                                           | 10                               |\n| `initialPatches`   | TravelPatches             | Restore saved patches when loading from storage                                                                                                                                 | {patches: [],inversePatches: []} |\n| `initialPosition`  | number                    | Restore position when loading from storage                                                                                                                                      | 0                                |\n| `autoArchive`      | boolean                   | Automatically save each change to history (see [Archive Mode](#archive-mode-control-when-changes-are-saved))                                                                    | true                             |\n| `mutable`          | boolean                   | Whether to mutate the state in place (for observable state like MobX, Vue, Pinia)                                                                                               | false                            |\n| `patchesOptions`   | boolean ｜ PatchesOptions | Customize JSON Patch format. Supports `{ pathAsArray: boolean }` to control path format. See [Mutative patches docs](https://mutative.js.org/docs/api-reference/create#patches) | `true` (enable patches)          |\n| `enableAutoFreeze` | boolean                   | Prevent accidental state mutations outside setState ([learn more](https://github.com/unadlib/mutative?tab=readme-ov-file#createstate-fn-options))                               | false                            |\n| `strict`           | boolean                   | Enable stricter immutability checks ([learn more](https://github.com/unadlib/mutative?tab=readme-ov-file#createstate-fn-options))                                               | false                            |\n| `mark`             | Mark\u003cO, F\u003e[]              | Mark certain objects as immutable ([learn more](https://github.com/unadlib/mutative?tab=readme-ov-file#createstate-fn-options))                                                 | () =\u003e void                       |\n\n**Returns:** `Travels\u003cS, F, A\u003e` - A Travels instance\n\n### Instance Methods\n\n#### `getState(): S`\n\nGet the current state.\n\n#### `setState(updater: S | (() =\u003e S) | ((draft: Draft\u003cS\u003e) =\u003e void)): void`\n\nUpdate the state. Supports three styles:\n\n- **Direct value:** `setState({ count: 1 })` - Replace state with a new object\n- **Function returning value:** `setState(() =\u003e ({ count: 1 }))` - Compute new state\n- **Draft mutation (recommended):** `setState((draft) =\u003e { draft.count = 1 })` - Mutate a draft copy\n\n\u003e **Performance Optimization:** Updates that produce no actual changes (empty patches) won't create history entries or trigger subscribers. For example, `setState(state =\u003e state)` or conditional updates that don't modify any fields. This prevents memory bloat from no-op operations.\n\n#### `subscribe(listener: (state, patches, position) =\u003e void): () =\u003e void`\n\nSubscribe to state changes. Returns an unsubscribe function.\n\n**Parameters:**\n\n- `listener`: Callback function called on state changes\n  - `state`: The new state\n  - `patches`: The current patches history\n  - `position`: The current position in history\n\n#### `back(amount?: number): void`\n\nUndo one or more changes by moving back in history. Defaults to 1 step.\n\n#### `forward(amount?: number): void`\n\nRedo one or more changes by moving forward in history. Defaults to 1 step.\n\n#### `go(position: number): void`\n\nJump to a specific position in the history timeline.\n\n#### `reset(): void`\n\nReset to the initial state and clear all history.\n\n#### `getHistory(): readonly S[]`\n\nReturns the complete history of states as an array.\n\n\u003e **IMPORTANT**: Do not modify the returned array. It is cached internally.\n\u003e In development mode, the array is frozen\n\u003e In production mode, modifications will corrupt the cache\n\n#### `getPosition(): number`\n\nReturns the current position in the history timeline.\n\n#### `getPatches(): TravelPatches`\n\nReturns the stored patches (the differences between states).\n\n#### `canBack(): boolean`\n\nReturns `true` if undo is possible (not at the beginning of history).\n\n#### `canForward(): boolean`\n\nReturns `true` if redo is possible (not at the end of history).\n\n#### `archive(): void` (Manual archive mode only)\n\nSaves the current state to history. Only available when `autoArchive: false`.\n\n#### `canArchive(): boolean` (Manual archive mode only)\n\nReturns `true` if there are unsaved changes that can be archived.\n\n#### `mutable: boolean`\n\nReturns whether mutable mode is enabled.\n\n#### `getControls(): TravelsControls | ManualTravelsControls`\n\nReturns a controls object containing all navigation methods and current state. Useful for passing to UI components without exposing the entire Travels instance. The controls object is cached and should be treated as read-only (it is frozen in development).\n\n```typescript\nconst travels = createTravels({ count: 0 });\nconst controls = travels.getControls();\n\n// Use controls\ncontrols.back();\ncontrols.forward();\nconsole.log(controls.position);\nconsole.log(controls.patches);\n```\n\n#### `maxHistory` option\n\nThe `maxHistory` option limits how many history entries (patches) are kept in memory. Older entries beyond this limit are automatically discarded to save memory.\n\n**How it works:**\n\n- `maxHistory` defines the maximum number of **patches** (changes), not states\n- When the limit is exceeded, the oldest patches are removed\n- The current `position` is capped at `maxHistory`, even if you make more changes\n- `reset()` can always return to the true initial state, regardless of history trimming\n\n**Example: Understanding the history window**\n\nIf you set `maxHistory: 3` and make 5 increments, here's what happens:\n\n```ts\nconst travels = createTravels({ count: 0 }, { maxHistory: 3 });\n\nconst controls = travels.getControls();\nconst increment = () =\u003e\n  travels.setState((draft) =\u003e {\n    draft.count += 1;\n  });\n\n// Make 5 changes\nincrement(); // 1\nincrement(); // 2\nincrement(); // 3\nincrement(); // 4\nincrement(); // 5\n\nexpect(travels.getState().count).toBe(5);\n\n// Position is capped at maxHistory (3), so we're at position 3\n// The library keeps only the last 3 patches, representing states: [2, 3, 4, 5]\n// Why 4 states? Because patches represent *transitions*:\n//   - patch 0: 2→3\n//   - patch 1: 3→4\n//   - patch 2: 4→5\n// So you can access 4 states total: the window start (2) plus 3 transitions\n\n// Go back 1 step: from 5 to 4\ncontrols.back();\nexpect(travels.getPosition()).toBe(2);\nexpect(travels.getState().count).toBe(4);\n\n// Go back 1 step: from 4 to 3\ncontrols.back();\nexpect(travels.getPosition()).toBe(1);\nexpect(travels.getState().count).toBe(3);\n\n// Go back 1 step: from 3 to 2 (the window start)\ncontrols.back();\nexpect(travels.getPosition()).toBe(0);\nexpect(travels.getState().count).toBe(2); // Can only go back to the window start\n\nexpect(controls.canBack()).toBe(false); // Can't go further back\n\n// However, reset() can still return to the true initial state\ncontrols.reset();\nexpect(travels.getState().count).toBe(0); // Back to the original initial state\n```\n\n## Mutable Mode: Keep Reactive State In Place\n\n`mutable: true` lets Travels mutate the same object reference you hand in. This is crucial for observable stores (MobX, Vue/Pinia, custom proxies) that depend on identity stability to trigger reactions. Under the hood, Travels still generates JSON Patches but applies them back to the live object via Mutative's `apply(..., { mutable: true })`, so undo/redo continues to work without allocating new objects.\n\n### When to Enable It\n\n- You pass a reactive store into `createTravels` and swapping the reference would break your observers.\n- You expect subscribers (`travels.subscribe`) to always receive the exact same object instance.\n- You batch multiple mutations with `autoArchive: false` but still need the UI to reflect every intermediate change.\n\nStick with the default immutable mode for reducer-driven stores (Redux, Zustand) where replacing the root object is the norm.\n\n### Behavior at a Glance\n\n- `setState` keeps the reference stable as long as the current state root is an object. Primitive roots (number, string, `null`) trigger an automatic immutable fallback plus a dev warning.\n- Function updaters that return a brand-new root (root replacement) also fall back to immutable assignment in mutable mode, with a dev warning.\n- No-op updates (producing empty patches) are optimized away and won't create history entries or notify subscribers.\n- `back`, `forward`, and `go` also mutate in place unless the history entry performs a root-level replacement (patch path `[]`). Those rare steps reassign the reference to keep history correct.\n- Root array time-travel in mutable mode can have ordering limitations; if you rely on array root navigation, prefer immutable mode or wrap the array in an object.\n- `reset` replays a diff from the original initial state, so the observable reference survives a reset.\n- `archive` (manual mode) merges temporary patches and still mutates the live object before saving history.\n- `getHistory()` reconstructs new objects from the stored patches. Treat them as read-only snapshots—they are not reactive proxies.\n- `subscribe` listeners always receive the live mutable object, so `state === travels.getState()` stays true.\n\n### Example: Pinia/Vue Store\n\n```ts\nimport { defineStore } from 'pinia';\nimport { reactive } from 'vue';\nimport { createTravels } from 'travels';\n\nexport const useTodosStore = defineStore('todos', () =\u003e {\n  const state = reactive({ items: [] });\n  const travels = createTravels(state, { mutable: true });\n  const controls = travels.getControls();\n\n  function addTodo(text: string) {\n    travels.setState((draft) =\u003e {\n      draft.items.push({ id: crypto.randomUUID(), text, done: false });\n    });\n  }\n\n  return { state, addTodo, controls };\n});\n```\n\nVue components keep using the original `state` reference while Travels tracks history and provides `controls` for undo/redo.\n\n### Limitations \u0026 Tips\n\n**JSON Serialization Requirements:**\n\nThe state must stay JSON-serializable because `reset()` relies on `deepClone(initialState)` for mutable mode. This has important implications:\n\n- ❌ **Date objects** → Converted to ISO strings (not restored as Date)\n\n  ```ts\n  {\n    createdAt: new Date();\n  } // Becomes: { createdAt: \"2025-01-15T...\" }\n  ```\n\n- ❌ **Map and Set** → Lost entirely (empty objects)\n\n  ```ts\n  {\n    tags: new Set(['a', 'b']);\n  } // Becomes: { tags: {} }\n  ```\n\n- ❌ **undefined values** → Removed from objects\n\n  ```ts\n  { name: 'Alice', age: undefined }  // Becomes: { name: 'Alice' }\n  ```\n\n- ⚠️ **Sparse arrays** → Mutable value updates fall back to immutable to preserve holes\n\n  ```ts\n  [1, , 3]; // Holes are preserved by falling back to immutable updates\n  ```\n\n- ❌ **Functions** → Lost entirely\n\n  ```ts\n  {\n    handler: () =\u003e {};\n  } // Becomes: { handler: undefined } → removed\n  ```\n\n- ❌ **Circular references** → Causes JSON.stringify error\n\n  ```ts\n  const obj = { self: null };\n  obj.self = obj; // ❌ TypeError: Converting circular structure to JSON\n  ```\n\n- ❌ **Class instances** → Converted to plain objects (lose methods/prototype)\n  ```ts\n  class User {\n    getName() {}\n  }\n  {\n    user: new User();\n  } // Becomes plain object without methods\n  ```\n\n**Workarounds:**\n\n- Store timestamps as numbers: `{ createdAt: Date.now() }`\n- Store Set/Map as arrays: `{ tags: ['a', 'b'] }`\n- Avoid undefined—use `null` instead\n- Serialize class instances before storing\n- Break circular references or use a custom serialization strategy\n\n**Other Tips:**\n\n- If you often replace the entire root object (e.g., `setState(() =\u003e newState)`) the library has to fall back to immutable jumps when navigating history. Prefer mutating the provided draft to keep reference sharing.\n- You can inspect `travels.mutable` at runtime to verify which mode is active.\n- See [`docs/mutable-mode.md`](docs/mutable-mode.md) for a deep dive, integration checklists, and troubleshooting tips.\n\n## Archive Mode: Control When Changes Are Saved\n\nTravels provides two ways to control when state changes are recorded in history:\n\n### Auto Archive Mode (default: `autoArchive: true`)\n\nIn auto archive mode, every `setState` call is automatically recorded as a separate history entry. This is the simplest mode and suitable for most use cases.\n\n```typescript\nconst travels = createTravels({ count: 0 });\n// or explicitly: createTravels({ count: 0 }, { autoArchive: true })\n\n// Each setState creates a new history entry\ntravels.setState({ count: 1 }); // History: [0, 1], position: 1\ntravels.setState({ count: 2 }); // History: [0, 1, 2], position: 2\ntravels.setState({ count: 3 }); // History: [0, 1, 2, 3], position: 3\n\n// No-op update - position stays the same (optimization)\ntravels.setState(state =\u003e state); // History: [0, 1, 2, 3], position: 3\n\n// Conditional update that changes nothing\ntravels.setState(draft =\u003e {\n  if (draft.count \u003e 10) {  // false, so no changes\n    draft.count = 0;\n  }\n}); // History: [0, 1, 2, 3], position: 3\n\ntravels.back(); // Go back to count: 2\n```\n\n### Manual Archive Mode (`autoArchive: false`)\n\nIn manual archive mode, you control when state changes are recorded to history using the `archive()` function. This is useful when you want to group multiple state changes into a single undo/redo step.\n\n**Use Case 1: Batch multiple changes into one history entry**\n\n```typescript\nconst travels = createTravels({ count: 0 }, { autoArchive: false });\n\n// Multiple setState calls\ntravels.setState({ count: 1 }); // Temporary change (not in history yet)\ntravels.setState({ count: 2 }); // Temporary change (not in history yet)\ntravels.setState({ count: 3 }); // Temporary change (not in history yet)\n\n// Commit all changes as a single history entry\ntravels.archive(); // History: [0, 3]\n\n// Now undo will go back to 0, not 2 or 1\ntravels.back(); // Back to 0\n```\n\n**Use Case 2: Explicit commit after a single change**\n\n```typescript\nfunction handleSave() {\n  travels.setState((draft) =\u003e {\n    draft.count += 1;\n  });\n  travels.archive(); // Commit immediately\n}\n```\n\n**Key Differences:**\n\n- **Auto archive**: Each `setState` = one undo step\n- **Manual archive**: `archive()` call = one undo step (can include multiple `setState` calls)\n\n## State Requirements: JSON-Serializable Only\n\nTravels stores and persists state using `deepClone(...)` internally. This makes reset and persistence fast and reliable, but **only JSON-serializable values are preserved**.\n\n**What works:** Objects, arrays, numbers, strings, booleans,`null`, and `Map`/`Set`(Supported only in immutable mode; not supported in mutable mode.).\n\n**What doesn't work:** `Date`, class instances, functions, or custom prototypes. These will either be converted (Date becomes an ISO string) or dropped entirely when history is reset or persisted.\n\n**Solution:** Convert complex types to simple representations before storing. For example, store timestamps as numbers instead of Date objects, or store IDs that reference external data instead of storing class instances directly.\n\nThis limitation applies even with the `mutable: true` option.\n\n## Framework Integration\n\n### React Integration\n\n```jsx\nimport { useSyncExternalStore } from 'react';\nimport { createTravels } from 'travels';\n\nconst travels = createTravels({ count: 0 });\n\nfunction useTravel() {\n  const state = useSyncExternalStore(\n    travels.subscribe.bind(travels),\n    travels.getState.bind(travels)\n  );\n\n  return [state, travels.setState.bind(travels), travels.getControls()] as const;\n}\n\nfunction Counter() {\n  const [state, setState, controls] = useTravel();\n\n  return (\n    \u003cdiv\u003e\n      \u003cdiv\u003eCount: {state.count}\u003c/div\u003e\n      \u003cbutton onClick={() =\u003e setState((draft) =\u003e { draft.count += 1; })}\u003e\n        Increment\n      \u003c/button\u003e\n      \u003cbutton onClick={() =\u003e controls.back()} disabled={!controls.canBack()}\u003e\n        Undo\n      \u003c/button\u003e\n      \u003cbutton onClick={() =\u003e controls.forward()} disabled={!controls.canForward()}\u003e\n        Redo\n      \u003c/button\u003e\n    \u003c/div\u003e\n  );\n}\n```\n\n### Zustand Integration\n\n```typescript\nimport { create } from 'zustand';\nimport { createTravels } from 'travels';\n\nconst travels = createTravels({ count: 0 });\n\nconst useStore = create((set) =\u003e ({\n  ...travels.getState(),\n  setState: (updater) =\u003e {\n    travels.setState(updater);\n    set(travels.getState());\n  },\n  controls: travels.getControls(),\n}));\n\n// Subscribe to travels changes\ntravels.subscribe((state) =\u003e {\n  useStore.setState(state);\n});\n```\n\n### Vue Integration\n\n```typescript\nimport { ref, readonly } from 'vue';\nimport { createTravels } from 'travels';\n\nexport function useTravel(initialState, options) {\n  const travels = createTravels(initialState, options);\n  const state = ref(travels.getState());\n\n  travels.subscribe((newState) =\u003e {\n    state.value = newState;\n  });\n\n  const setState = (updater) =\u003e {\n    travels.setState(updater);\n  };\n\n  return {\n    state: readonly(state),\n    setState,\n    controls: travels.getControls(),\n  };\n}\n```\n\n## Persistence: Saving History to Storage\n\nTo persist state across browser sessions or page reloads, save the current state, patches, and position. When reloading, pass these values as `initialState`, `initialPatches`, and `initialPosition`:\n\n```typescript\n// Save to localStorage\nfunction saveToStorage(travels) {\n  localStorage.setItem('state', JSON.stringify(travels.getState()));\n  localStorage.setItem('patches', JSON.stringify(travels.getPatches()));\n  localStorage.setItem('position', JSON.stringify(travels.getPosition()));\n}\n\n// Load from localStorage\nfunction loadFromStorage() {\n  const initialState = JSON.parse(localStorage.getItem('state') || '{}');\n  const initialPatches = JSON.parse(\n    localStorage.getItem('patches') || '{\"patches\":[],\"inversePatches\":[]}'\n  );\n  const initialPosition = JSON.parse(localStorage.getItem('position') || '0');\n\n  return createTravels(initialState, {\n    initialPatches,\n    initialPosition,\n  });\n}\n```\n\n## TypeScript Support\n\n`travels` is written in TypeScript and provides full type definitions.\n\n```typescript\nimport {\n  createTravels,\n  type TravelsOptions,\n  type TravelPatches,\n} from 'travels';\n\ninterface State {\n  count: number;\n  todos: Array\u003c{ id: number; text: string }\u003e;\n}\n\nconst travels = createTravels\u003cState\u003e({ count: 0, todos: [] });\n\n// Type-safe state updates\ntravels.setState((draft) =\u003e {\n  draft.count += 1;\n  draft.todos.push({ id: 1, text: 'Buy milk' });\n});\n```\n\n## Advanced: Extending Travels with Custom Logic\n\nYou can enhance Travels by wrapping its methods to add validation, permissions, logging, rate limiting, and other custom behaviors.\n\n**Common use cases:**\n\n- ✅ **Validation** - Prevent invalid state changes before they're applied\n- ✅ **Permissions** - Control who can undo/redo or modify state\n- ✅ **Logging \u0026 Auditing** - Track all state changes for debugging or compliance\n- ✅ **Metadata** - Automatically add timestamps, user IDs, or version numbers\n- ✅ **Rate Limiting** - Throttle frequent updates to prevent performance issues\n- ✅ **History Overflow Detection** - Archive old history to external storage\n\n**Quick example:**\n\n```typescript\nconst travels = createTravels({ count: 0 });\nconst originalSetState = travels.setState.bind(travels);\n\n// Add validation\ntravels.setState = function (updater: any) {\n  if (typeof updater === 'object' \u0026\u0026 updater.count \u003e 100) {\n    console.error('Count cannot exceed 100');\n    return; // Block the operation\n  }\n  return originalSetState(updater);\n} as any;\n```\n\n**📖 Full documentation:** See [Advanced Patterns Guide](docs/advanced-patterns.md) for:\n\n- Complete examples with both direct values and mutation functions\n- Composable wrapper patterns (validation, logging, permissions)\n- Real-world integration patterns\n- TypeScript-safe implementation techniques\n\n## Related Projects\n\n- [use-travel](https://github.com/mutativejs/use-travel) - React hook for time travel\n- [zustand-travel](https://github.com/mutativejs/zustand-travel) - Zustand middleware for time travel\n- [mutative](https://github.com/unadlib/mutative) - Efficient immutable updates\n\n## License\n\nMIT\n","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmutativejs%2Ftravels","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmutativejs%2Ftravels","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmutativejs%2Ftravels/lists"}