{"id":21850391,"url":"https://github.com/mutativejs/use-travel","last_synced_at":"2026-04-25T16:05:24.656Z","repository":{"id":228213693,"uuid":"766544931","full_name":"mutativejs/use-travel","owner":"mutativejs","description":"A React hook for state time travel with undo, redo, reset and archive functionalities.","archived":false,"fork":false,"pushed_at":"2025-11-15T08:45:07.000Z","size":491,"stargazers_count":88,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-12-17T06:37:21.438Z","etag":null,"topics":["immutable","mutative","react","react-hooks","redo","state-history","time-travel","undo","use-undo"],"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":"2024-03-03T15:05:44.000Z","updated_at":"2025-11-29T03:04:43.000Z","dependencies_parsed_at":"2024-03-17T17:25:35.005Z","dependency_job_id":"f716539d-3c90-4337-9230-df744a904d22","html_url":"https://github.com/mutativejs/use-travel","commit_stats":{"total_commits":74,"total_committers":2,"mean_commits":37.0,"dds":"0.027027027027026973","last_synced_commit":"2d392701faf33761aaef3ad18691b5752ee01afe"},"previous_names":["unadlib/use-travel","mutativejs/use-travel"],"tags_count":33,"template":false,"template_full_name":null,"purl":"pkg:github/mutativejs/use-travel","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mutativejs%2Fuse-travel","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mutativejs%2Fuse-travel/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mutativejs%2Fuse-travel/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mutativejs%2Fuse-travel/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mutativejs","download_url":"https://codeload.github.com/mutativejs/use-travel/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mutativejs%2Fuse-travel/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28752672,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-25T10:25:12.305Z","status":"ssl_error","status_checked_at":"2026-01-25T10:25:11.933Z","response_time":113,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: 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":["immutable","mutative","react","react-hooks","redo","state-history","time-travel","undo","use-undo"],"created_at":"2024-11-28T00:17:19.912Z","updated_at":"2026-04-25T16:05:24.636Z","avatar_url":"https://github.com/mutativejs.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# use-travel\n\n![Node CI](https://github.com/mutativejs/use-travel/workflows/Node%20CI/badge.svg)\n[![Coverage Status](https://coveralls.io/repos/github/mutativejs/use-travel/badge.svg?branch=main)](https://coveralls.io/github/mutativejs/use-travel?branch=main)\n[![npm](https://img.shields.io/npm/v/use-travel.svg)](https://www.npmjs.com/package/use-travel)\n![license](https://img.shields.io/npm/l/use-travel)\n\n**React hooks for [Travels](https://github.com/mutativejs/travels): patch-based undo/redo state with immutable updates, manual archiving, rebasing, and shared-store support.**\n\n`use-travel` is the React layer for [`travels`](https://github.com/mutativejs/travels). It keeps the same core model as Travels, which stores JSON Patch history instead of full state snapshots, but exposes that model through React-friendly hooks:\n\n- `useTravel` for component-scoped state with undo/redo\n- `useTravelStore` for subscribing React components to an existing immutable `Travels` instance\n\nUse plain [`travels`](https://github.com/mutativejs/travels) directly when your state lives outside React, you need imperative reads right after navigation, or you need `mutable: true`.\n\n## Table of Contents\n\n- [Why use-travel?](#why-use-travel)\n- [Installation](#installation)\n- [Quick Start](#quick-start)\n- [Choosing Between `useTravel`, `useTravelStore`, and `travels`](#choosing-between-usetravel-usetravelstore-and-travels)\n- [API Reference](#api-reference)\n  - [`useTravel(initialState, options?)`](#usetravelinitialstate-options)\n  - [`useTravelStore(travels)`](#usetravelstoretravels)\n- [Archive Modes](#archive-modes)\n- [Important Behavior](#important-behavior)\n- [Rebase](#rebase)\n- [Persistence](#persistence)\n- [State Requirements](#state-requirements)\n- [Examples](#examples)\n- [Related Projects](#related-projects)\n- [License](#license)\n\n## Why use-travel?\n\n- **React-first API**: Use a hook tuple instead of wiring subscriptions manually.\n- **Patch-based history**: Undo/redo stores only changes, not full state snapshots.\n- **Mutative update syntax**: Write `draft.count += 1` while keeping immutable React state.\n- **Manual archive mode**: Group several edits into one undo step when needed.\n- **Rebase support**: Promote the current state to the new reset baseline.\n- **Shared history support**: Subscribe multiple React components to the same immutable `Travels` store with `useTravelStore`.\n\n## Installation\n\n```bash\nnpm install use-travel travels mutative\n# or\nyarn add use-travel travels mutative\n# or\npnpm add use-travel travels mutative\n```\n\n### Version compatibility\n\n| use-travel | travels                                    |\n| ---------- | ------------------------------------------ |\n| `\u003e= 1.8.0` | `\u003e= 1.2.0` (required for `rebase` support) |\n| `\u003c 1.8.0`  | `\u003c 1.2.0`                                  |\n\n## Quick Start\n\n```tsx\nimport { useTravel } from 'use-travel';\n\nexport function Counter() {\n  const [state, setState, controls] = useTravel({ count: 0 });\n\n  return (\n    \u003cdiv\u003e\n      \u003cstrong\u003e{state.count}\u003c/strong\u003e\n\n      \u003cbutton\n        onClick={() =\u003e\n          setState((draft) =\u003e {\n            draft.count += 1;\n          })\n        }\n      \u003e\n        Increment\n      \u003c/button\u003e\n\n      \u003cbutton onClick={() =\u003e controls.back()} disabled={!controls.canBack()}\u003e\n        Undo\n      \u003c/button\u003e\n\n      \u003cbutton\n        onClick={() =\u003e controls.forward()}\n        disabled={!controls.canForward()}\n      \u003e\n        Redo\n      \u003c/button\u003e\n\n      \u003cbutton onClick={controls.reset}\u003eReset\u003c/button\u003e\n    \u003c/div\u003e\n  );\n}\n```\n\n`setState` supports three update styles:\n\n- Direct value: `setState({ count: 1 })`\n- Function returning a value: `setState(() =\u003e ({ count: 1 }))`\n- Draft mutation: `setState((draft) =\u003e { draft.count += 1 })`\n\n## Choosing Between `useTravel`, `useTravelStore`, and `travels`\n\n- Use `useTravel` when the state belongs to a React component and React should own the lifecycle.\n- Use `useTravelStore` when you already have a shared immutable `Travels` instance and want React to stay subscribed to it.\n- Use plain [`travels`](https://github.com/mutativejs/travels) when another layer is the source of truth, you need imperative `getState()` reads after `back()` or `forward()`, or you need `mutable: true`.\n\n## API Reference\n\n### `useTravel(initialState, options?)`\n\nCreates a component-scoped immutable `Travels` instance and returns a tuple:\n\n```ts\nconst [state, setState, controls] = useTravel(initialState, options);\n```\n\n`useTravel` always uses immutable mode internally so React can observe state changes through reference updates. `mutable` is intentionally not supported here.\n\n#### Options\n\n| Option                 | Type             | Description                                                                       | Default                               |\n| ---------------------- | ---------------- | --------------------------------------------------------------------------------- | ------------------------------------- |\n| `maxHistory`           | `number`         | Maximum number of history entries to keep                                         | `10`                                  |\n| `initialPatches`       | `TravelPatches`  | Patch history to restore from persistence                                         | `{ patches: [], inversePatches: [] }` |\n| `strictInitialPatches` | `boolean`        | Throw when persisted patches are invalid instead of falling back to empty history | `false`                               |\n| `initialPosition`      | `number`         | History position to restore from persistence                                      | `0`                                   |\n| `autoArchive`          | `boolean`        | Save each change automatically or require manual `archive()`                      | `true`                                |\n| `enableAutoFreeze`     | `boolean`        | Forwarded to Mutative immutability options                                        | `false`                               |\n| `strict`               | `boolean`        | Forwarded to Mutative strict immutability checks                                  | `false`                               |\n| `mark`                 | `Mark\u003cO, F\u003e[]`   | Forwarded to Mutative mark options                                                | `() =\u003e void`                          |\n| `patchesOptions`       | `PatchesOptions` | Customize patch output such as `{ pathAsArray: true }`                            | enabled                               |\n\n#### Returns\n\nCommon tuple members:\n\n| Member                      | Type                         | Description                                                                 |\n| --------------------------- | ---------------------------- | --------------------------------------------------------------------------- |\n| `state`                     | `Value\u003cS, F\u003e`                | Current render snapshot                                                     |\n| `setState`                  | `Updater\u003cS\u003e`                 | Updates state with a value, function, or draft mutation                     |\n| `controls.position`         | `number`                     | Current position in the history timeline                                    |\n| `controls.getHistory()`     | `() =\u003e Value\u003cS, F\u003e[]`        | Returns the history as state snapshots                                      |\n| `controls.patches`          | `TravelPatches`              | Returns the stored patch history                                            |\n| `controls.back(amount?)`    | `(amount?: number) =\u003e void`  | Undo one or more steps                                                      |\n| `controls.forward(amount?)` | `(amount?: number) =\u003e void`  | Redo one or more steps                                                      |\n| `controls.go(position)`     | `(position: number) =\u003e void` | Jump to a specific history position                                         |\n| `controls.reset()`          | `() =\u003e void`                 | Reset to the initial state and clear history                                |\n| `controls.rebase()`         | `() =\u003e void`                 | Make the current state the new baseline and discard past and future history |\n| `controls.canBack()`        | `() =\u003e boolean`              | Whether undo is possible                                                    |\n| `controls.canForward()`     | `() =\u003e boolean`              | Whether redo is possible                                                    |\n\nWhen `autoArchive: false`, the controls also include:\n\n| Member                  | Type            | Description                                            |\n| ----------------------- | --------------- | ------------------------------------------------------ |\n| `controls.archive()`    | `() =\u003e void`    | Commit the current working state as the next undo step |\n| `controls.canArchive()` | `() =\u003e boolean` | Whether there are unarchived changes                   |\n\n### `useTravelStore(travels)`\n\nSubscribes React to an existing immutable `Travels` instance without creating a new store.\n\n```tsx\n// store.ts\nimport { Travels } from 'travels';\n\nexport const travels = new Travels({ count: 0 });\n```\n\n```tsx\n// Counter.tsx\nimport { useTravelStore } from 'use-travel';\nimport { travels } from './store';\n\nexport function Counter() {\n  const [state, setState, controls] = useTravelStore(travels);\n\n  return (\n    \u003cdiv\u003e\n      \u003cspan\u003e{state.count}\u003c/span\u003e\n      \u003cbutton\n        onClick={() =\u003e\n          setState((draft) =\u003e {\n            draft.count += 1;\n          })\n        }\n      \u003e\n        Increment\n      \u003c/button\u003e\n      \u003cbutton onClick={() =\u003e controls.back()} disabled={!controls.canBack()}\u003e\n        Undo\n      \u003c/button\u003e\n    \u003c/div\u003e\n  );\n}\n```\n\nImportant notes for `useTravelStore`:\n\n- It only supports immutable `Travels` instances. Passing a store created with `mutable: true` throws.\n- It exposes the same navigation controls as `useTravel`, including `rebase()`.\n- It is a React bridge, so the returned `state` is still a render snapshot.\n- If you need imperative \"navigate and read immediately\" behavior, call `travels.back()` or `travels.forward()` and read `travels.getState()` directly from the store.\n\n## Archive Modes\n\n`use-travel` supports two recording modes.\n\n### Auto Archive Mode\n\nWith the default `autoArchive: true`, every `setState` call becomes its own undo step.\n\n```tsx\nconst [state, setState, controls] = useTravel({ count: 0 });\n\nfunction increment() {\n  setState((draft) =\u003e {\n    draft.count += 1;\n  });\n}\n\n// Three separate user interactions:\n// click #1 -\u003e count = 1\n// click #2 -\u003e count = 2\n// click #3 -\u003e count = 3\n\ncontrols.back(); // { count: 2 }\n```\n\n### Manual Archive Mode\n\nWith `autoArchive: false`, you decide when the current working state should become a committed history entry.\n\nThis is useful for flows like forms, drag interactions, or multi-step editors where several changes should undo together.\n\n```tsx\nconst [doc, setDoc, controls] = useTravel(\n  { title: '', body: '' },\n  { autoArchive: false }\n);\n\nfunction onTitleChange(title: string) {\n  setDoc((draft) =\u003e {\n    draft.title = title;\n  });\n}\n\nfunction onBodyChange(body: string) {\n  setDoc((draft) =\u003e {\n    draft.body = body;\n  });\n}\n\nfunction save() {\n  if (controls.canArchive()) {\n    controls.archive();\n  }\n}\n```\n\n## Important Behavior\n\n### One `setState` call per synchronous call stack\n\n`useTravel` throws if `setState` is called more than once within the same synchronous call stack. If multiple fields need to change together, update them in a single draft mutation.\n\n```tsx\nsetState((draft) =\u003e {\n  draft.count += 1;\n  draft.todos.push({ id: 1, text: 'Buy milk' });\n});\n```\n\nIn manual archive mode, you can still make one `setState` call per event or render and archive later when the grouped change is ready.\n\n### `initialState` and `options` are read once\n\n`useTravel` creates the underlying `Travels` instance only on the first render. Later changes to `initialState` or `options` do not recreate the history store automatically. If you need a fresh store, remount the component or change its `key`.\n\n### No-op updates are ignored\n\nUpdates that do not produce actual changes do not create history entries.\n\n## Rebase\n\n`controls.rebase()` discards all past and future history and makes the current state the new baseline.\n\nThis is a destructive operation. After rebasing:\n\n- `controls.position` becomes `0`\n- `controls.getHistory()` contains only the current state\n- `controls.reset()` returns to the rebased state, not the original initial state\n- In manual archive mode, any unarchived working changes become part of the new baseline\n\n```tsx\nconst [state, setState, controls] = useTravel({ count: 0 });\n\nsetState((draft) =\u003e {\n  draft.count = 5;\n});\n\ncontrols.rebase();\n\nsetState((draft) =\u003e {\n  draft.count = 9;\n});\n\ncontrols.reset(); // { count: 5 }\n```\n\n## Persistence\n\n`use-travel` re-exports `TravelPatches`, so you can persist both the current state and its history:\n\n```tsx\nimport type { TravelPatches } from 'use-travel';\n\ntype SavedTravel = {\n  state: { count: number };\n  patches: TravelPatches;\n  position: number;\n};\n\nconst saved: SavedTravel = {\n  state,\n  patches: controls.patches,\n  position: controls.position,\n};\n```\n\nRestore that data by passing the saved state as `initialState` and the saved history as `initialPatches` plus `initialPosition`:\n\n```tsx\nconst [state, setState, controls] = useTravel(saved.state, {\n  initialPatches: saved.patches,\n  initialPosition: saved.position,\n});\n```\n\nIf persisted patch data may be corrupt, set `strictInitialPatches: true` to fail fast instead of silently starting with empty history.\n\n## State Requirements\n\n`use-travel` follows the same state rules as [`travels`](https://github.com/mutativejs/travels):\n\n- Prefer plain JSON-serializable data.\n- `Map` and `Set` are supported in immutable mode.\n- Avoid complex mutable objects such as class instances, functions, DOM nodes, or framework-specific reactive proxies.\n\nIf you need mutable observable state, use `travels` directly instead of `useTravelStore`.\n\n## Examples\n\n- [Basic](https://stackblitz.com/edit/react-xfw3uk?file=src%2FApp.js)\n- [Manual Time Travel](https://stackblitz.com/edit/react-3mnzq9?file=src%2FApp.js)\n\n## Related Projects\n\n- [travels](https://github.com/mutativejs/travels) - The framework-agnostic undo/redo core\n- [zustand-travel](https://github.com/mutativejs/zustand-travel) - Zustand middleware built on Travels\n\n## License\n\n`use-travel` is [MIT licensed](https://github.com/mutativejs/use-travel/blob/main/LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmutativejs%2Fuse-travel","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmutativejs%2Fuse-travel","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmutativejs%2Fuse-travel/lists"}