Skip to content

Commit a893a45

Browse files
authored
Merge pull request #62 from chrisvander/fix-immer
fix: immer middleware throwing
2 parents e3b96ef + a599978 commit a893a45

4 files changed

Lines changed: 71 additions & 23 deletions

File tree

bun.lockb

1.58 KB
Binary file not shown.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"@types/bun": "^1.1.13",
1818
"@types/node": "^22.9.0",
1919
"husky": "^9.1.6",
20+
"immer": "^10.1.3",
2021
"react": "^18.3.1",
2122
"semver": "^7.6.3",
2223
"typescript": "^5.6.3",

src/computed.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { beforeEach, describe, expect, mock, test } from "bun:test"
22
import { type StateCreator, create } from "zustand"
3+
import { immer } from "zustand/middleware/immer"
34
import { type ComputedStateOpts, createComputed } from "./computed"
45

56
type Store = {
@@ -196,3 +197,43 @@ describe("slices pattern", () => {
196197
expect(computeSliceMock).toHaveBeenCalledTimes(3)
197198
})
198199
})
200+
201+
describe("immer middleware functions without throwing", () => {
202+
type Store = {
203+
count: number
204+
inc: () => void
205+
dec: () => void
206+
}
207+
208+
type ComputedStore = {
209+
countSq: number
210+
}
211+
212+
const computed = createComputed(
213+
(state: Store): ComputedStore => ({
214+
countSq: state.count ** 2,
215+
}),
216+
{ keys: ["count"] },
217+
)
218+
219+
const useStore = create<Store>()(
220+
immer(
221+
computed((set) => ({
222+
count: 1,
223+
inc: () =>
224+
set((state) => {
225+
// example with Immer middleware
226+
state.count += 1
227+
}),
228+
dec: () => set((state) => ({ count: state.count - 1 })),
229+
})),
230+
),
231+
)
232+
233+
expect(() => useStore.getState().inc()).not.toThrow()
234+
expect(useStore.getState().count).toEqual(2)
235+
expect(useStore.getState().countSq).toEqual(4)
236+
expect(() => useStore.getState().dec()).not.toThrow()
237+
expect(useStore.getState().count).toEqual(1)
238+
expect(useStore.getState().countSq).toEqual(1)
239+
})

src/computed.ts

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export type ComputedStateOpts<T> = (
2222
* should be *fast* - it determines whether or not you need to
2323
* recompute.
2424
*/
25-
shouldRecompute?: (state: T, nextState: T) => boolean
25+
shouldRecompute?: (state: T, nextState: T | Partial<T>) => boolean
2626
}
2727
) & {
2828
/**
@@ -76,18 +76,14 @@ type ComputedStateImpl = <T extends object, A extends object>(
7676
opts?: ComputedStateOpts<T>,
7777
) => (f: StateCreator<T, [], []>) => StateCreator<T, [], [], T & A>
7878

79-
type SetStateWithArgs = Parameters<ReturnType<ReturnType<ComputedStateImpl>>>[0] extends (...args: infer U) => void
80-
? (...args: [...U, ...unknown[]]) => void
81-
: never
82-
8379
const computedImpl: ComputedStateImpl = (compute, opts) => (f) => {
8480
type T = ReturnType<typeof f>
8581
type A = ReturnType<typeof compute>
8682

8783
const optsKeys = !opts || !("keys" in opts) || opts.keys == null ? undefined : opts.keys
8884
const keysSet = optsKeys ? new Set(optsKeys as string[]) : undefined
8985

90-
function defaultShouldRecomputeFn<T>(_: T, nextState: T): boolean {
86+
function defaultShouldRecomputeFn<T>(_: T, nextState: T | Partial<T>): boolean {
9187
if (!keysSet || nextState == null) return true
9288
return Object.keys(nextState).some((k) => keysSet.has(k))
9389
}
@@ -99,39 +95,49 @@ const computedImpl: ComputedStateImpl = (compute, opts) => (f) => {
9995
return (set, get, api) => {
10096
const equalityFn = opts?.equalityFn ?? shallow
10197

102-
const computeAndMerge = (state: T | (T & A)): T & A => {
98+
function computeAndMerge(state: T | (T & A)): T & A {
10399
// Calculate the new computed state.
104-
const computedState: A = compute({ ...state })
100+
const computedState = compute(state)
105101

106102
// If part of the computed state did not change according to the equalityFn,
107103
// then delete that key from the newly calculated computed state.
108104
for (const k of Object.keys(computedState) as (keyof A)[]) {
109-
if (equalityFn(computedState[k], (state as T & A)[k])) {
105+
if (k in state && equalityFn(computedState[k], (state as T & A)[k])) {
110106
delete computedState[k]
111107
}
112108
}
113109

114-
return { ...state, ...computedState }
110+
return Object.assign(state, computedState)
115111
}
116112

113+
const _api = api as Mutate<StoreApi<T>, [["chrisvander/zustand-computed", A]]>
114+
117115
/**
118116
* Higher level function to handle compute & compare overhead.
119117
*/
120-
const setWithComputed = (update: T | ((state: T) => T), replace?: boolean, ...args: unknown[]) => {
121-
;(set as SetStateWithArgs)(
122-
(state: T): T & A => {
123-
const updated = typeof update === "object" ? update : update(state)
124-
if (!shouldRecomputeFn?.(state, updated)) return { ...state, ...updated } as T & A
125-
return computeAndMerge({ ...state, ...updated })
126-
},
127-
replace,
128-
...args,
129-
)
118+
function setState(partial: T | Partial<T> | ((state: T) => T | Partial<T>), replace?: false): void
119+
function setState(state: T | ((state: T) => T), replace: true): void
120+
function setState(arg: T | Partial<T> | ((state: T) => T | Partial<T>), replace?: boolean): void {
121+
if (replace === false || replace == null) {
122+
// Merge the partial state with the current state.
123+
set((state) => {
124+
const newState = typeof arg === "function" ? arg(state) : arg
125+
if (!shouldRecomputeFn(state, newState)) return newState
126+
return computeAndMerge(Object.assign(state, newState))
127+
}, replace)
128+
return
129+
}
130+
131+
set((state) => {
132+
const newArg = arg as T | ((state: T) => T)
133+
const newState: T = typeof newArg === "function" ? newArg(state) : newArg
134+
if (!shouldRecomputeFn(state, newState)) return newState
135+
return computeAndMerge(newState)
136+
}, replace)
130137
}
131138

132-
const _api = api as Mutate<StoreApi<T>, [["chrisvander/zustand-computed", A]]>
133-
_api.setState = setWithComputed
134-
const st = f(setWithComputed, get, _api) as T & A
139+
_api.setState = setState
140+
const st = f(setState, get, _api)
135141
return Object.assign({}, st, compute(st))
136142
}
137143
}

0 commit comments

Comments
 (0)