Skip to content

Commit

Permalink
init states (#81)
Browse files Browse the repository at this point in the history
  • Loading branch information
charkour authored Apr 23, 2023
1 parent e8e10df commit c8acf00
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 23 deletions.
50 changes: 40 additions & 10 deletions apps/web/pages/separate.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
import { temporal } from "zundo";
import create from "zustand";
import { temporal } from 'zundo';
import create from 'zustand';

interface MyState {
bears: number;
bees: number;
increment: () => void;
decrement: () => void;
incrementBees: () => void;
decrementBees: () => void;
}

const useStore = create(
temporal<MyState>((set) => ({
bears: 0,
bees: 10,
increment: () => set((state) => ({ bears: state.bears + 1 })),
decrement: () => set((state) => ({ bears: state.bears - 1 })),
})),
incrementBees: () => set((state) => ({ bees: state.bees + 1 })),
decrementBees: () => set((state) => ({ bees: state.bees - 1 })),
}), {
pastStates: [{ bees: 20}, { bees: 30 }],
}),
);
const useTemporalStore = create(useStore.temporal);

Expand All @@ -34,8 +42,12 @@ const UndoBar = () => {
);
};

const StateBar = () => {
const store = useStore();
const StateBear = () => {
const store = useStore((state) => ({
bears: state.bears,
increment: state.increment,
decrement: state.decrement,
}));
const { bears, increment, decrement } = store;
return (
<div>
Expand All @@ -50,24 +62,42 @@ const StateBar = () => {
);
};

const StateBee = () => {
const store = useStore();
console.log(store)
const { bees, increment, decrement } = store;
return (
<div>
current state: {JSON.stringify(store)}
<br />
<br />
bees: {bees}
<br />
<button onClick={increment}>increment</button>
<button onClick={decrement}>decrement</button>
</div>
);
};

const App = () => {
return (
<div>
<h1>
{" "}
{' '}
<span role="img" aria-label="bear">
🐻
</span>{" "}
</span>{' '}
<span role="img" aria-label="recycle">
♻️
</span>{" "}
</span>{' '}
Zundo!
</h1>
<StateBar />
<StateBear />
<StateBee />
<br />
<UndoBar />
</div>
);
};

export default App;
export default App;
26 changes: 25 additions & 1 deletion packages/zundo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,9 @@ zundo has one export: `temporal`. It is used to as middleware for `create` from
### Middleware Options

```tsx
type onSave<TState> = (pastState: TState, currentState: TState) => void;
type onSave<TState> =
| ((pastState: TState, currentState: TState) => void)
| undefined;

export interface ZundoOptions<TState, PartialTState = TState> {
partialize?: (state: TState) => PartialTState;
Expand All @@ -123,6 +125,8 @@ export interface ZundoOptions<TState, PartialTState = TState> {
handleSet?: (
handleSet: StoreApi<TState>['setState'],
) => StoreApi<TState>['setState'];
pastStates?: Partial<PartialTState>[];
futureStates?: Partial<PartialTState>[];
}
```

Expand Down Expand Up @@ -233,6 +237,26 @@ const withTemporal = temporal<MyState>(
);
```

#### **Initialize temporal store with past and future states**

`pastStates?: Partial<PartialTState>[]`

`futureStates?: Partial<PartialTState>[]`

You can initialize the temporal store with past and future states. This is useful when you want to load a previous state from a database or initialize the store with a default state. By default, the temporal store is initialized with an empty array of past and future states.

> Note: The `pastStates` and `futureStates` do not respect the limit set in the options. If you want to limit the number of past and future states, you must do so manually prior to initializing the store.
```tsx
const withTemporal = temporal<MyState>(
(set) => ({ ... }),
{
pastStates: [{ field1: 'value1' }, { field1: 'value2' }],
futureStates: [{ field1: 'value3' }, { field1: 'value4' }],
},
);
```

### `useStore.temporal`

When using zustand with the `temporal` middleware, a `temporal` object is attached to your vanilla or React-based store. `temporal` is a vanilla zustand store: see [StoreApi<T> from](https://github.com/pmndrs/zustand/blob/f0ff30f7c431f6bf25b3cb439d065a7e61355df4/src/vanilla.ts#L8) zustand for more details.
Expand Down
69 changes: 65 additions & 4 deletions packages/zundo/__tests__/options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ import throttle from '../node_modules/lodash.throttle';
interface MyState {
count: number;
count2: number;
myString: string;
string2: string;
boolean1: boolean;
boolean2: boolean;
increment: () => void;
decrement: () => void;
doNothing: () => void;
Expand All @@ -28,6 +32,10 @@ const createVanillaStore = (
return {
count: 0,
count2: 0,
myString: 'hello',
string2: 'world',
boolean1: true,
boolean2: false,
increment: () =>
set((state) => ({
count: state.count + 1,
Expand Down Expand Up @@ -76,13 +84,21 @@ describe('Middleware options', () => {
increment: expect.any(Function),
decrement: expect.any(Function),
doNothing: expect.any(Function),
myString: 'hello',
string2: 'world',
boolean1: true,
boolean2: false,
});
expect(store.temporal.getState().pastStates[1]).toEqual({
count: 1,
count2: 1,
increment: expect.any(Function),
decrement: expect.any(Function),
doNothing: expect.any(Function),
myString: 'hello',
string2: 'world',
boolean1: true,
boolean2: false,
});
expect(store.getState()).toContain({ count: 2, count2: 2 });
});
Expand All @@ -93,8 +109,6 @@ describe('Middleware options', () => {
count: state.count,
}),
});
const { pastStates, futureStates } =
storeWithPartialize.temporal.getState();
expect(storeWithPartialize.temporal.getState().pastStates.length).toBe(0);
expect(storeWithPartialize.temporal.getState().futureStates.length).toBe(
0,
Expand All @@ -119,8 +133,7 @@ describe('Middleware options', () => {
count: state.count,
}),
});
const { undo, pastStates, futureStates } =
storeWithPartialize.temporal.getState();
const { undo } = storeWithPartialize.temporal.getState();
expect(storeWithPartialize.temporal.getState().pastStates.length).toBe(0);
expect(storeWithPartialize.temporal.getState().futureStates.length).toBe(
0,
Expand All @@ -143,6 +156,10 @@ describe('Middleware options', () => {
increment: expect.any(Function),
decrement: expect.any(Function),
doNothing: expect.any(Function),
boolean1: true,
boolean2: false,
myString: 'hello',
string2: 'world',
});
act(() => {
undo();
Expand All @@ -159,6 +176,10 @@ describe('Middleware options', () => {
increment: expect.any(Function),
decrement: expect.any(Function),
doNothing: expect.any(Function),
boolean1: true,
boolean2: false,
myString: 'hello',
string2: 'world',
});
});
});
Expand Down Expand Up @@ -537,4 +558,44 @@ describe('Middleware options', () => {
// TODO: should this check the equality function, limit, and call onSave? These are already tested but indirectly.
});
});

describe('init pastStates', () => {
it('should init the pastStates with the initial state', () => {
const storeWithPastStates = createVanillaStore({
pastStates: [{ count: 0 }, { count: 1 }],
});
expect(storeWithPastStates.temporal.getState().pastStates.length).toBe(2);
});
it('should be able to call undo on init pastStates', () => {
const storeWithPastStates = createVanillaStore({
pastStates: [{ count: 999 }, { count: 1000 }],
});
expect(storeWithPastStates.getState().count).toBe(0);
act(() => {
storeWithPastStates.temporal.getState().undo();
});
expect(storeWithPastStates.getState().count).toBe(1000);
});
});

describe('init futureStates', () => {
it('should init the futureStates with the initial state', () => {
const storeWithFutureStates = createVanillaStore({
futureStates: [{ count: 0 }, { count: 1 }],
});
expect(
storeWithFutureStates.temporal.getState().futureStates.length,
).toBe(2);
});
it('should be able to call redo on init futureStates', () => {
const storeWithFutureStates = createVanillaStore({
futureStates: [{ count: 1001 }, { count: 1000 }],
});
expect(storeWithFutureStates.getState().count).toBe(0);
act(() => {
storeWithFutureStates.temporal.getState().redo();
});
expect(storeWithFutureStates.getState().count).toBe(1000);
});
});
});
10 changes: 6 additions & 4 deletions packages/zundo/src/temporal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ export const createVanillaTemporal = <TState>(
userSet: StoreApi<TState>['setState'],
userGet: StoreApi<TState>['getState'],
partialize: (state: TState) => TState,
{ equality, onSave, limit } = {} as Omit<ZundoOptions<TState>, 'handleSet'>,
{ equality, onSave, limit, pastStates = [], futureStates = [] } = {} as Omit<
ZundoOptions<TState>,
'handleSet'
>,
) => {

return createStore<TemporalStateWithInternals<TState>>((set, get) => {
return {
pastStates: [],
futureStates: [],
pastStates,
futureStates,
undo: (steps = 1) => {
// Fastest way to clone an array on Chromium. Needed to create a new array reference
const pastStates = get().pastStates.slice();
Expand Down
12 changes: 8 additions & 4 deletions packages/zundo/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import type { StoreApi } from 'zustand';

type onSave<TState> = ((pastState: TState, currentState: TState) => void) | undefined;
type onSave<TState> =
| ((pastState: TState, currentState: TState) => void)
| undefined;

export interface TemporalStateWithInternals<TState> {
pastStates: TState[];
futureStates: TState[];
pastStates: Partial<TState>[];
futureStates: Partial<TState>[];

undo: (steps?: number) => void;
redo: (steps?: number) => void;
Expand All @@ -27,6 +29,8 @@ export interface ZundoOptions<TState, PartialTState = TState> {
handleSet?: (
handleSet: StoreApi<TState>['setState'],
) => StoreApi<TState>['setState'];
pastStates?: Partial<PartialTState>[];
futureStates?: Partial<PartialTState>[];
}

export type Write<T, U> = Omit<T, keyof U> & U;
Expand All @@ -37,4 +41,4 @@ export type TemporalState<TState> = Omit<
>;

// https://stackoverflow.com/a/69328045/9931154
export type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] }
export type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] };

0 comments on commit c8acf00

Please sign in to comment.