Skip to content

Commit

Permalink
initially support v5 (#189)
Browse files Browse the repository at this point in the history
  • Loading branch information
charkour authored Nov 17, 2024
1 parent ce4511d commit c9ebf3e
Show file tree
Hide file tree
Showing 22 changed files with 780 additions and 797 deletions.
7 changes: 6 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ jobs:
strategy:
matrix:
node-version: [16.x, 20.x]
zustand: [4.2.0, 4.0.0]
zustand: [
4.2.0, # Oldest zustand TS supported
4.0.0, # Oldest zustand JS supported
4, # Latest zustand v5 supported
latest # Latest zustand supported
]

steps:
- uses: actions/checkout@v4
Expand Down
22 changes: 15 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Try a live [demo](https://codesandbox.io/s/currying-flower-2dom9?file=/src/App.t
npm i zustand zundo
```

> zustand v4.2.0 or higher is required for TS usage. v4.0.0 or higher is required for JS usage.
> zustand v4.2.0+ or v5 is required for TS usage. v4.0.0 or higher is required for JS usage.
> Node 16 or higher is required.
## Background
Expand Down Expand Up @@ -87,13 +87,21 @@ const App = () => {
In React, to subscribe components or custom hooks to member properties of the `temporal` object (like the array of `pastStates` or `currentStates`), you can create a `useTemporalStore` hook.

```tsx
import { useStore } from 'zustand';
import { useStoreWithEqualityFn } from 'zustand/traditional';
import type { TemporalState } from 'zundo';

const useTemporalStore = <T,>(
selector: (state: TemporalState<StoreState>) => T,
function useTemporalStore(): TemporalState<MyState>;
function useTemporalStore<T>(selector: (state: TemporalState<MyState>) => T): T;
function useTemporalStore<T>(
selector: (state: TemporalState<MyState>) => T,
equality: (a: T, b: T) => boolean,
): T;
function useTemporalStore<T>(
selector?: (state: TemporalState<MyState>) => T,
equality?: (a: T, b: T) => boolean,
) => useStore(useStoreWithUndo.temporal, selector, equality);
) {
return useStoreWithEqualityFn(useStoreWithUndo.temporal, selector!, equality);
}

const App = () => {
const { bears, increasePopulation, removeAllBears } = useStoreWithUndo();
Expand Down Expand Up @@ -224,8 +232,7 @@ const useStoreWithUndo = create<StoreState>()(
const useTemporalStore = <T,>(
// Use partalized StoreState type as the generic here
selector: (state: TemporalState<PartializedStoreState>) => T,
equality?: (a: T, b: T) => boolean,
) => useStore(useStoreWithUndo.temporal, selector, equality);
) => useStore(useStoreWithUndo.temporal, selector);
```

### Limit number of historical states stored
Expand Down Expand Up @@ -558,6 +565,7 @@ PRs are welcome! [pnpm](https://pnpm.io/) is used as a package manager. Run `pnp
- [SubscribeWithSelector](https://codesandbox.io/s/zundo-with-subscribe-with-selector-forked-mug69t)
- [canUndo, canRedo, undoDepth, redoDepth](https://codesandbox.io/s/zundo-canundo-and-undodepth-l6jclx?file=/src/App.tsx:572-731)
- [with deep equal](https://codesandbox.io/p/sandbox/zundo-deep-equal-qg69lj)
- [with input](https://stackblitz.com/edit/vitejs-vite-jqngm9?file=src%2FApp.tsx)

## Migrate from v1 to v2

Expand Down
10 changes: 5 additions & 5 deletions examples/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,18 @@
"lodash.merge": "4.6.2",
"lodash.throttle": "4.1.1",
"microdiff": "1.4.0",
"next": "14.2.14",
"next": "14.2.15",
"react": "18.3.1",
"react-dom": "18.3.1",
"zundo": "workspace:*",
"zustand": "4.5.2"
"zustand": "5.0.0"
},
"devDependencies": {
"@types/lodash.merge": "4.6.9",
"@types/lodash.throttle": "4.1.9",
"@types/node": "22.7.4",
"@types/node": "22.7.6",
"@types/react": "18.3.11",
"eslint": "9.11.1",
"typescript": "5.6.2"
"eslint": "9.13.0",
"typescript": "5.6.3"
}
}
2 changes: 1 addition & 1 deletion examples/web/pages/chained.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { temporal } from 'zundo';
import create from 'zustand';
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';

Expand Down
18 changes: 14 additions & 4 deletions examples/web/pages/diff.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { temporal, type TemporalState } from 'zundo';
import { useStore, createStore } from 'zustand';
import { createStore } from 'zustand';
import { shallow } from 'zustand/shallow';
import diff from 'microdiff';
import { useStoreWithEqualityFn } from 'zustand/traditional';

interface MyState {
count: number;
Expand Down Expand Up @@ -51,11 +52,20 @@ const originalStore = createStore(withZundo);
const useBaseStore = <T,>(
selector: (state: MyState) => T,
equality?: (a: T, b: T) => boolean,
) => useStore(originalStore, selector, equality);
const useTemporalStore = <T,>(
) => useStoreWithEqualityFn(originalStore, selector, equality);

function useTemporalStore(): TemporalState<MyState>;
function useTemporalStore<T>(selector: (state: TemporalState<MyState>) => T): T;
function useTemporalStore<T>(
selector: (state: TemporalState<MyState>) => T,
equality: (a: T, b: T) => boolean,
): T;
function useTemporalStore<T>(
selector?: (state: TemporalState<MyState>) => T,
equality?: (a: T, b: T) => boolean,
) => useStore(originalStore.temporal, selector, equality);
) {
return useStoreWithEqualityFn(originalStore.temporal, selector!, equality);
}

const isEmpty = (obj: object) => {
for (const _ in obj) {
Expand Down
7 changes: 4 additions & 3 deletions examples/web/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import throttle from 'lodash.throttle';
import { temporal, type TemporalState } from 'zundo';
import { useStore, createStore } from 'zustand';
import { createStore } from 'zustand';
import { shallow } from 'zustand/shallow';
import { useStoreWithEqualityFn } from 'zustand/traditional';

interface MyState {
count: number;
Expand Down Expand Up @@ -29,11 +30,11 @@ const originalStore = createStore(withZundo);
const useBaseStore = <T,>(
selector: (state: MyState) => T,
equality?: (a: T, b: T) => boolean,
) => useStore(originalStore, selector, equality);
) => useStoreWithEqualityFn(originalStore, selector, equality);
const useTemporalStore = <T,>(
selector: (state: TemporalState<MyState>) => T,
equality?: (a: T, b: T) => boolean,
) => useStore(originalStore.temporal, selector, equality);
) => useStoreWithEqualityFn(originalStore.temporal, selector, equality);

export default function Web() {
const { count, increment, decrement } = useBaseStore((state) => state);
Expand Down
7 changes: 4 additions & 3 deletions examples/web/pages/input.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { temporal, type TemporalState } from 'zundo';
import { useStore, createStore } from 'zustand';
import { createStore } from 'zustand';
import { shallow } from 'zustand/shallow';
import { useStoreWithEqualityFn } from 'zustand/traditional';

interface MyState {
fontSize: number;
Expand All @@ -17,11 +18,11 @@ const originalStore = createStore(withZundo);
const useBaseStore = <T extends unknown>(
selector: (state: MyState) => T,
equality?: (a: T, b: T) => boolean,
) => useStore(originalStore, selector, equality);
) => useStoreWithEqualityFn(originalStore, selector, equality);
const useTemporalStore = <T extends unknown>(
selector: (state: TemporalState<MyState>) => T,
equality?: (a: T, b: T) => boolean,
) => useStore(originalStore.temporal, selector, equality);
) => useStoreWithEqualityFn(originalStore.temporal, selector, equality);

export default function App() {
const { fontSize, changeFontSize } = useBaseStore((state) => state);
Expand Down
174 changes: 174 additions & 0 deletions examples/web/pages/nested.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import { temporal, type TemporalState } from 'zundo';
import {
create,
type StateCreator,
type StoreMutatorIdentifier,
} from 'zustand';
import { useStoreWithEqualityFn } from 'zustand/traditional';

interface MyState {
incrementBears: () => void;
incrementUntrackedValue: () => void;
incrementAll: () => void;
incrementSimple: () => void;
simple: number;
nested: { bears: number; untrackedValue: number };
}

type HistoryTrackedState = Omit<MyState, 'untrackedValue'>;

// Note: This one is incomplete
const middle = <
T extends object,
Mps extends [StoreMutatorIdentifier, unknown][] = [],
Mcs extends [StoreMutatorIdentifier, unknown][] = [],
>(
config: StateCreator<T, Mps, Mcs>,
): StateCreator<T, Mps, Mcs> => {
const foo: StateCreator<T, Mps, Mcs> = (_set, get, store) => {
const set: typeof _set = (state, replace) => {
if (state instanceof Function) {
_set(mergeDeep(get(), state(get())), replace);
return;
}
_set(mergeDeep(get(), state), replace);
};
store.setState = set;
return config(set, get, store);
};
return foo;
};

const useMyStore = create<MyState>()(
middle(
temporal(
(set) => ({
simple: 0,
nested: { bears: 0, untrackedValue: 0 },
incrementBears: () =>
set(({ nested: { bears, untrackedValue } }) => ({
nested: { bears: bears + 1, untrackedValue },
})),
incrementAll: () =>
set(({ nested: { bears, untrackedValue }, simple }) => ({
nested: { bears: bears + 1, untrackedValue: untrackedValue + 1 },
simple: simple + 1,
})),
incrementUntrackedValue: () =>
set(({ nested: { bears, untrackedValue } }) => ({
nested: { bears: bears, untrackedValue: untrackedValue + 1 },
})),
incrementSimple: () => set(({ simple }) => ({ simple: simple + 1 })),
}),
{
partialize: (state): HistoryTrackedState => {
const { nested } = state;
// TODO: recursive partial
return { nested: { bears: nested.bears } };
},
},
),
),
);

/**
* Performs a deep merge of objects and returns new object. Does not modify
* objects (immutable) and merges arrays via concatenation.
*
* @param {...object} objects - Objects to merge
* @returns {object} New object with merged key/values
* Citation: {@link https://stackoverflow.com/a/48218209/9931154 Stack Overflow Reference}
*/
function mergeDeep(...objects: any[]) {
const isObject = (obj: unknown) => obj && typeof obj === 'object';

return objects.reduce((prev, obj) => {
Object.keys(obj).forEach((key) => {
const pVal = prev[key];
const oVal = obj[key];

if (Array.isArray(pVal) && Array.isArray(oVal)) {
prev[key] = pVal.concat(...oVal);
} else if (isObject(pVal) && isObject(oVal)) {
prev[key] = mergeDeep(pVal, oVal);
} else {
prev[key] = oVal;
}
});

return prev;
}, {});
}

const useTemporalStore = <T,>(
selector: (state: TemporalState<HistoryTrackedState>) => T,
equality?: (a: T, b: T) => boolean,
) => useStoreWithEqualityFn(useMyStore.temporal, selector, equality);

const App = () => {
const store = useMyStore();
const {
simple,
nested,
incrementUntrackedValue,
incrementAll,
incrementBears,
incrementSimple,
} = store;
const { undo, redo, clear, futureStates, pastStates } = useTemporalStore(
(state) => state,
);

return (
<div>
<h1>
{' '}
<span role="img" aria-label="bear">
🐻
</span>{' '}
<span role="img" aria-label="recycle">
♻️
</span>{' '}
Zundo!
</h1>
<h2>
With config options: <br />
partialize, handleSet, equality
</h2>
<p>The throttle value is set to 500ms.</p>
<p>untrackedValue is not tracked in history (partialize)</p>
<p>equality function is fast-deep-equal</p>
<p>
Note that clicking the button that increments untrackedValue prior to
incrementing bears results in state history of bears not being tracked
</p>
<button onClick={incrementAll}>increment all</button>
<br /> <br />
<button onClick={incrementBears}>increment bears</button>
<br /> <br />
<button onClick={incrementUntrackedValue}>increment untracked</button>
<br /> <br />
<button onClick={incrementSimple}>increment simple</button>
<br /> <br />
<button onClick={() => undo()}>undo</button>
<button onClick={() => redo()}>redo</button>
<button onClick={() => clear()}>clear</button>
<br /> <br />
past states: {JSON.stringify(pastStates)}
<br />
future states: {JSON.stringify(futureStates)}
<br />
current state: {JSON.stringify(store)}
<br />
<br />
nested.bears: {nested.bears}
<br />
nested.untrackedValue: {nested.untrackedValue}
<br />
simple: {simple}
<br />
</div>
);
};

export default App;
8 changes: 4 additions & 4 deletions examples/web/pages/persist.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useMemo } from 'react';
import { create } from 'zustand';
import { create, useStore } from 'zustand';
import { temporal } from 'zundo';
import { persist, type PersistOptions } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
Expand All @@ -16,7 +16,7 @@ const persistOptions: PersistOptions<Store> = {
name: 'some-store',
};

const useStore = create<Store>()(
const useMyStore = create<Store>()(
persist(
temporal(
immer((set) => ({
Expand Down Expand Up @@ -44,11 +44,11 @@ const useStore = create<Store>()(
),
);

const useTemporalStore = create(useStore.temporal);
const useTemporalStore = () => useStore(useMyStore.temporal);

export const Persist = dynamic(
Promise.resolve(() => {
const state = useStore();
const state = useMyStore();
const temporalState = useTemporalStore();

const localStorageStateOnLoad = useMemo(
Expand Down
Loading

0 comments on commit c9ebf3e

Please sign in to comment.