- Notifications
You must be signed in to change notification settings - Fork1
A React hook for state time travel with undo, redo, reset and archive functionalities.
License
mutativejs/use-travel
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
A React hook for state time travel with undo, redo, reset and archive functionalities withTravels.
use-travel is a small and high-performance library for state time travel. It's built onMutative andTravels to support mutation updating immutable data. It's designed to be simple and easy to use, and it's also customizable for different use cases.
It's suitable for building any time travel feature in your application.
npm install use-travel mutative travels# oryarn add use-travel mutative travels# orpnpm add use-travel mutative travels
- Undo/Redo/Reset/Go/Archive functionalities
- Mutations update immutable data
- Small size for time travel with JSON Patch history
- Customizable history size
- Customizable initial patches
- High performance
- Mark function for custom immutability
You can useuseTravel to create a time travel state. And it returns a tuple with the current state, the state setter, and the controls. The controls includeback(),forward(),reset(),canBack(),canForward(),canArchive(),getHistory(),patches,position,archive(), andgo().
import{useTravel}from'use-travel';constApp=()=>{const[state,setState,controls]=useTravel(0,{maxHistory:10,initialPatches:{patches:[],inversePatches:[],},});return(<div><div>{state}</div><buttononClick={()=>setState(state+1)}>Increment</button><buttononClick={()=>setState(state-1)}>Decrement</button><buttononClick={()=>controls.back()}disabled={!controls.canBack()}> Undo</button><buttononClick={()=>controls.forward()}disabled={!controls.canForward()}> Redo</button><buttononClick={controls.reset}>Reset</button>{controls.getHistory().map((state,index)=>(<divkey={index}>{state}</div>))}{controls.patches.patches.map((patch,index)=>(<divkey={index}>{JSON.stringify(patch)}</div>))}<div>{controls.position}</div><buttononClick={()=>{controls.go(1);}}> Go</button></div>);};
| Parameter | type | description | default |
|---|---|---|---|
maxHistory | number | The maximum number of history to keep | 10 |
initialPatches | TravelPatches | The initial patches | {patches: [],inversePatches: []} |
initialPosition | number | The initial position of the state | 0 |
autoArchive | boolean | Auto archive the state (seeArchive Mode for details) | true |
enableAutoFreeze | boolean | Enable auto freeze the state,view more | false |
strict | boolean | Enable strict mode,view more | false |
mark | Mark<O, F>[] | The mark function ,view more | () => void |
| Return | type | description |
|---|---|---|
state | Value<S, F> | The current state |
setState | Updater<InitialValue | The state setter, support mutation update or return immutable data |
controls.back | (amount?: number) => void | Go back to the previous state |
controls.forward | (amount?: number) => void | Go forward to the next state |
controls.reset | () => void | Reset the state to the initial state |
controls.canBack | () => boolean | Check if can go back to the previous state |
controls.canForward | () => boolean | Check if can go forward to the next state |
controls.canArchive | () => boolean | Check if can archive the current state |
controls.getHistory | () => T[] | Get the history of the state |
controls.patches | TravelPatches[] | Get the patches history of the state |
controls.position | number | Get the current position of the state |
controls.go | (nextPosition: number) => void | Go to the specific position of the state |
controls.archive | () => void | Archive the current state(theautoArchive options should befalse) |
When you need to manage a singleTravels instance outside of React—e.g. to share the same undo/redo history across multiple components—create the store manually and bind it withuseTravelStore. The hook keeps React in sync with the external store, exposes the same controls object, and rejects mutable stores to ensure React can observe updates.
// store.tsimport{Travels}from'travels';exportconsttravels=newTravels({count:0});// mutable: true is not supported
// Counter.tsximport{useTravelStore}from'use-travel';import{travels}from'./store';exportfunctionCounter(){const[state,setState,controls]=useTravelStore(travels);return(<div><span>{state.count}</span><buttononClick={()=>setState((draft)=>{draft.count+=1;})}> Increment</button><buttononClick={()=>controls.back()}disabled={!controls.canBack()}> Undo</button></div>);}
useTravelStore stays reactive even when theTravels instance is updated elsewhere (for example, in services or other components) and forwards manual archive helpers when the store is created withautoArchive: false.
use-travel provides two archive modes to control how state changes are recorded in history:
In auto archive mode, everysetState call is automatically recorded as a separate history entry. This is the simplest mode and suitable for most use cases.
const[state,setState,controls]=useTravel({count:0});// or explicitly: useTravel({ count: 0 }, { autoArchive: true })// Each setState creates a new history entrysetState({count:1});// History: [0, 1]// ... user clicks another buttonsetState({count:2});// History: [0, 1, 2]// ... user clicks another buttonsetState({count:3});// History: [0, 1, 2, 3]controls.back();// Go back to count: 2
In manual archive mode, you control when state changes are recorded to history using thearchive() function. This is useful when you want to group multiple state changes into a single undo/redo step.
Use Case 1: Batch multiple changes into one history entry
const[state,setState,controls]=useTravel({count:0},{autoArchive:false});// Multiple setState calls across different renderssetState({count:1});// Temporary change (not in history yet)// ... user clicks another buttonsetState({count:2});// Temporary change (not in history yet)// ... user clicks another buttonsetState({count:3});// Temporary change (not in history yet)// Commit all changes as a single history entrycontrols.archive();// History: [0, 3]// Now undo will go back to 0, not 2 or 1controls.back();// Back to 0
Use Case 2: Explicit commit after a single change
functionhandleSave(){setState((draft)=>{draft.count+=1;});controls.archive();// Commit immediately}
The key difference:
- Auto archive: Each
setState= one undo step - Manual archive:
archive()call = one undo step (can include multiplesetStatecalls)
⚠️ setState Restriction:setStatecan only be calledonce within the same synchronous call stack (e.g., inside a single event handler). This ensures predictable undo/redo behavior where each history entry represents a clear, atomic change.
constApp=()=>{const[state,setState,controls]=useTravel({count:0,todo:[]});return(<div><div>{state.count}</div><buttononClick={()=>{// ❌ Multiple setState calls in the same event handlersetState((draft)=>{draft.count+=1;});setState((draft)=>{draft.todo.push({id:1,text:'Buy'});});// This will throw: "setState cannot be called multiple times in the same render cycle"// ✅ Correct: Batch all changes in a single setStatesetState((draft)=>{draft.count+=1;draft.todo.push({id:1,text:'Buy'});});}}> Update</button></div>);};
Note: With
autoArchive: false, you can callsetStateonce per event handler across multiple renders, then callarchive()whenever you want to commit those changes to history.
TravelPatchesis the type of patches history, it includespatchesandinversePatches.
If you want to persist the state, you can use
state/controls.patches/controls.positionto save the travel history. Then, read the persistent data asinitialState,initialPatches, andinitialPositionwhen initializing the state, like this:
const[state,setState,controls]=useTravel(initialState,{ initialPatches, initialPosition,});
use-travel isMIT licensed.
About
A React hook for state time travel with undo, redo, reset and archive functionalities.
Topics
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Packages0
Uh oh!
There was an error while loading.Please reload this page.