🍜 undo/redo middleware for zustand. <700 bytes
handleSet
arguments to include currentState
and deltaState
(#139) in #149
Full Changelog: https://github.com/charkour/zundo/compare/v2.0.3...v2.1.0
Full Changelog: https://github.com/charkour/zundo/compare/v2.0.2...v2.0.3
v2.0.1 attempted to support ESM and CJS, but it instead changed the module support from CJS to ESM which is a potentially breaking change based on the user's JS runtime specification. This change fixes the package.json
file to support ESM or CJS, depending on what's required by the user's runtime.
Full Changelog: https://github.com/charkour/zundo/compare/v2.0.1...v2.0.2
cjs
in addition to esm
nowFull Changelog: https://github.com/charkour/zundo/compare/v2.0.0...v2.0.1
v2.0.0 is a complete rewrite of zundo. It is smaller and more flexible. It also has a smaller bundle size and allows you to opt into specific performance trade-offs. The API has changed slightly. See the API section for more details. Below is a summary of the changes as well as steps to migrate from v1 to v2.
v2 introduces 8 new middleware options and is only ~800 bytes where v1 was a few KBs.
zundo
is used by several projects and teams including Stability AI, Yext, KaotoIO, and NutSH.ai.
If this library is useful to you, please consider sponsoring the project. Thank you!
PRs are welcome! pnpm is used as a package manager. Run pnpm install
to install local dependencies. Thank you for contributing!
include
and exclude
options are now handled by the partialize
option.allowUnchanged
option is now handled by the equality
option. By default, all state changes are tracked. In v1, we bundled lodash.isequal
to handle equality checks. In v2, you are able to use any function.historyDepthLimit
option has been renamed to limit
.coolOffDurationMs
option is now handled by the handleSet
option by wrapping the setter function with a throttle or debounce function.temporal
rather than undoMiddleware
.partialize
option to omit or include specific fields. By default, the entire state object is tracked.limit
option to limit the number of previous and future states stored in history.equality
option to use a custom equality function to determine when a state change should be tracked. By default, all state changes are tracked.diff
option to store state delta rather than full object.onSave
option to call a function when the temporal store is updated.handleSet
option to throttle or debounce state changes.pastStates
and futureStates
options to initialize the temporal store with past and future states.wrapTemporal
option to wrap the temporal store with middleware. The temporal
store is a vanilla zustand store.temporal.getState()
APIundo
, redo
, and clear
functions are now always defined. They can no longer be undefined
.undo()
and redo()
functions now accept an optional steps
parameter to go back or forward multiple states at once.isTracking
flag, and pause
, and resume
functions are now available on the temporal store.setOnSave
function is now available on the temporal store to change the onSave
behavior after the store has been created.- import { undoMiddleware } from 'zundo';
+ import { temporal } from 'zundo';
include
or exclude
, use the new partialize
option// v1.6.0
// Only field1 and field2 will be tracked
const useStoreA = create<StoreState>(
undoMiddleware(
set => ({ ... }),
{ include: ['field1', 'field2'] }
)
);
// Everything besides field1 and field2 will be tracked
const useStoreB = create<StoreState>(
undoMiddleware(
set => ({ ... }),
{ exclude: ['field1', 'field2'] }
)
);
// v2.0.0
// Only field1 and field2 will be tracked
const useStoreA = create<StoreState>(
temporal(
(set) => ({
// your store fields
}),
{
partialize: (state) => {
const { field1, field2, ...rest } = state;
return { field1, field2 };
},
},
),
);
// Everything besides field1 and field2 will be tracked
const useStoreB = create<StoreState>(
temporal(
(set) => ({
// your store fields
}),
{
partialize: (state) => {
const { field1, field2, ...rest } = state;
return rest;
},
},
),
);
allowUnchanged
, use the new equality
option// v1.6.0
// Use an existing `allowUnchanged` option
const useStore = create<StoreState>(
undoMiddleware(
set => ({ ... }),
{ allowUnchanged: true }
)
);
// v2.0.0
// Use an existing equality function
import { shallow } from 'zustand/shallow'; // or use `lodash.isequal` or any other equality function
// Use an existing equality function
const useStoreA = create<StoreState>(
temporal(
(set) => ({
// your store fields
}),
{ equality: shallow },
),
);
historyDepthLimit
, use the new limit
option// v1.6.0
// Use an existing `historyDepthLimit` option
const useStore = create<StoreState>(
undoMiddleware(
set => ({ ... }),
{ historyDepthLimit: 100 }
)
);
// v2.0.0
// Use `limit` option
const useStore = create<StoreState>(
temporal(
(set) => ({
// your store fields
}),
{ limit: 100 },
),
);
coolOffDurationMs
, use the new handleSet
option// v1.6.0
// Use an existing `coolOffDurationMs` option
const useStore = create<StoreState>(
undoMiddleware(
set => ({ ... }),
{ coolOfDurationMs: 1000 }
)
);
// v2.0.0
// Use `handleSet` option
const withTemporal = temporal<MyState>(
(set) => ({
// your store fields
}),
{
handleSet: (handleSet) =>
throttle<typeof handleSet>((state) => {
console.info('handleSet called');
handleSet(state);
}, 1000),
},
);
diff
option to track state deltas by @charkour in https://github.com/charkour/zundo/pull/123
Full Changelog: https://github.com/charkour/zundo/compare/v1.6.0...v2.0.0
Allows for runtime perf which results in a larger bundle size. More space efficient but less time efficient.
819 B --> 848 B (3.54% increase)
diff?: (pastState: Partial<PartialTState>, currentState: Partial<PartialTState>) => Partial<PartialTState> | null
For performance reasons, you may want to store the state delta rather than the complete (potentially partialized) state object. This can be done by passing a diff
function. The diff
function should return an object that represents the difference between the past and current state. By default, the full state object is stored.
If diff
returns null
, the state change will not be tracked. This is helpful for a conditionally storing past states or if you have a doNothing
action that does not change the state.
You can write your own or use something like microdiff
, just-diff
, or deep-object-diff
.
const useStore = create<StoreState>(
temporal(
(set) => ({
// your store fields
}),
{
diff: (pastState, currentState) => {
const myDiff = diff(currentState, pastState);
const newStateFromDiff = myDiff.reduce(
(acc, difference) => {
type Key = keyof typeof currentState;
if (difference.type === 'CHANGE') {
const pathAsString = difference.path.join('.') as Key;
acc[pathAsString] = difference.value;
}
return acc;
},
{} as Partial<typeof currentState>,
);
return isEmpty(newStateFromDiff) ? null : newStateFromDiff;
},
},
),
);
diff
option to track state deltas by @charkour in https://github.com/charkour/zundo/pull/123
Full Changelog: https://github.com/charkour/zundo/compare/v2.0.0-beta.24...v2.0.0-beta.25
821 B --> 819 B
Full Changelog: https://github.com/charkour/zundo/compare/v2.0.0-beta.23...v2.0.0-beta.24
833 B --> 821 B
Switch from using a while loop to using array methods. This allows us to shrink the bundle size and to only perform array copies if we are actually going to commit something to the store.
Full Changelog: https://github.com/charkour/zundo/compare/v2.0.0-beta.22...v2.0.0-beta.23
839 B --> 833 B
Full Changelog: https://github.com/charkour/zundo/compare/v2.0.0-beta.21...v2.0.0-beta.22
852 B --> 839 B
- trackingStatus: 'tracking' | 'paused'
+ isTracking: boolean
- trackingStatus === 'tracking'
+ isTracking === true
- trackingStatus === 'paused'
+ isTracking === false
isTracking: boolean
isTracking
: a stateful flag in the temporal
store that indicates whether the temporal
store is tracking state changes or not. Possible values are true
or false
. To programmatically pause and resume tracking, use pause()
and resume()
explained below.
pause: () => void
pause
: call function to pause tracking state changes. This will prevent new states from being stored in history within the temporal store. Sets isTracking
to false
.
resume: () => void
resume
: call function to resume tracking state changes. This will allow new states to be stored in history within the temporal store. Sets isTracking
to true
.
Full Changelog: https://github.com/charkour/zundo/compare/v2.0.0-beta.20...v2.0.0-beta.21