Skip to content

Commit 7e94455

Browse files
authored
fix(middleware): simplify middleware types and allow explicit typing (#617)
* chore(tests): add a use case combining many middleware * simplify middleware types and allow explicit typing reverting some of #601 * fix context tests * prettier * refine middleware type test for more coverage and readability
1 parent 06a60bb commit 7e94455

File tree

4 files changed

+489
-543
lines changed

4 files changed

+489
-543
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
"semi": false,
6868
"trailingComma": "es5",
6969
"singleQuote": true,
70-
"jsxBracketSameLine": true,
70+
"bracketSameLine": true,
7171
"tabWidth": 2,
7272
"printWidth": 80
7373
},

src/middleware.ts

Lines changed: 67 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@ type DevtoolsType = {
2323
error: (payload: any) => void
2424
}
2525

26+
export type StoreApiWithRedux<
27+
T extends State,
28+
A extends { type: unknown }
29+
> = StoreApi<T & { dispatch: (a: A) => A }> & {
30+
dispatch: (a: A) => A
31+
devtools?: DevtoolsType
32+
}
33+
2634
export const redux =
2735
<S extends State, A extends { type: unknown }>(
2836
reducer: (state: S, action: A) => S,
@@ -31,10 +39,7 @@ export const redux =
3139
(
3240
set: SetState<S & { dispatch: (a: A) => A }>,
3341
get: GetState<S & { dispatch: (a: A) => A }>,
34-
api: StoreApi<S & { dispatch: (a: A) => A }> & {
35-
dispatch: (a: A) => A
36-
devtools?: DevtoolsType
37-
}
42+
api: StoreApiWithRedux<S, A>
3843
): S & { dispatch: (a: A) => A } => {
3944
api.dispatch = (action: A) => {
4045
set((state: S) => reducer(state, action))
@@ -59,24 +64,18 @@ export type NamedSet<T extends State> = {
5964
): void
6065
}
6166

67+
export type StoreApiWithDevtools<T extends State> = StoreApi<T> & {
68+
devtools?: DevtoolsType
69+
}
70+
6271
export const devtools =
6372
<
6473
S extends State,
65-
InnerCustomSetState extends NamedSet<S>,
66-
InnerCustomGetState extends GetState<S>,
67-
InnerCustomStoreApi extends StoreApi<S> & {
68-
dispatch?: unknown
69-
devtools?: DevtoolsType
70-
},
71-
OuterCustomSetState extends SetState<S>,
72-
OuterCustomGetState extends InnerCustomGetState,
73-
OuterCustomStoreApi extends InnerCustomStoreApi
74+
CustomSetState extends SetState<S>,
75+
CustomGetState extends GetState<S>,
76+
CustomStoreApi extends StoreApiWithDevtools<S>
7477
>(
75-
fn: (
76-
set: InnerCustomSetState,
77-
get: InnerCustomGetState,
78-
api: InnerCustomStoreApi
79-
) => S,
78+
fn: (set: NamedSet<S>, get: CustomGetState, api: CustomStoreApi) => S,
8079
options?:
8180
| string
8281
| {
@@ -99,9 +98,9 @@ export const devtools =
9998
}
10099
) =>
101100
(
102-
set: OuterCustomSetState,
103-
get: OuterCustomGetState,
104-
api: OuterCustomStoreApi
101+
set: CustomSetState,
102+
get: CustomGetState,
103+
api: CustomStoreApi & { dispatch?: unknown }
105104
): S => {
106105
let extension
107106
try {
@@ -118,23 +117,15 @@ export const devtools =
118117
console.warn('Please install/enable Redux devtools extension')
119118
}
120119
delete api.devtools
121-
return fn(
122-
set as unknown as InnerCustomSetState,
123-
get as InnerCustomGetState,
124-
api as InnerCustomStoreApi
125-
)
120+
return fn(set, get, api)
126121
}
127122
const namedSet: NamedSet<S> = (state, replace, name) => {
128123
set(state, replace)
129124
if (!api.dispatch && api.devtools) {
130125
api.devtools.send(api.devtools.prefix + (name || 'action'), get())
131126
}
132127
}
133-
const initialState = fn(
134-
namedSet as InnerCustomSetState,
135-
get as InnerCustomGetState,
136-
api as InnerCustomStoreApi
137-
)
128+
const initialState = fn(namedSet, get, api)
138129
if (!api.devtools) {
139130
const savedSetState = api.setState
140131
api.setState = <
@@ -213,79 +204,35 @@ export const devtools =
213204
return initialState
214205
}
215206

216-
type SubscribeWithSelector<T extends State> = {
217-
(listener: StateListener<T>): () => void
218-
<StateSlice>(
219-
selector: StateSelector<T, StateSlice>,
220-
listener: StateSliceListener<StateSlice>,
221-
options?: {
222-
equalityFn?: EqualityChecker<StateSlice>
223-
fireImmediately?: boolean
224-
}
225-
): () => void
226-
}
227-
228-
export function subscribeWithSelector<S extends State>(
229-
fn: (
230-
set: SetState<S>,
231-
get: GetState<S>,
232-
api: Omit<StoreApi<S>, 'subscribe'> & {
233-
subscribe: SubscribeWithSelector<S>
234-
subscribeWithSelectorEnabled: true
235-
}
236-
) => S
237-
): (
238-
set: SetState<S>,
239-
get: GetState<S>,
240-
api: Omit<StoreApi<S>, 'subscribe'> & {
241-
subscribe: SubscribeWithSelector<S>
242-
subscribeWithSelectorEnabled: true
207+
export type StoreApiWithSubscribeWithSelector<T extends State> = Omit<
208+
StoreApi<T>,
209+
'subscribe'
210+
> & {
211+
subscribe: {
212+
(listener: StateListener<T>): () => void
213+
<StateSlice>(
214+
selector: StateSelector<T, StateSlice>,
215+
listener: StateSliceListener<StateSlice>,
216+
options?: {
217+
equalityFn?: EqualityChecker<StateSlice>
218+
fireImmediately?: boolean
219+
}
220+
): () => void
243221
}
244-
) => S
222+
// Note: This will be removed in v4
223+
subscribeWithSelectorEnabled: true
224+
}
245225

246-
export function subscribeWithSelector<
247-
S extends State,
248-
CustomSetState extends SetState<S>
249-
>(
250-
fn: (
251-
set: CustomSetState,
252-
get: GetState<S>,
253-
api: Omit<StoreApi<S>, 'subscribe'> & {
254-
subscribe: SubscribeWithSelector<S>
255-
subscribeWithSelectorEnabled: true
256-
}
257-
) => S
258-
): (
259-
set: CustomSetState,
260-
get: GetState<S>,
261-
api: Omit<StoreApi<S>, 'subscribe'> & {
262-
subscribe: SubscribeWithSelector<S>
263-
subscribeWithSelectorEnabled: true
264-
}
265-
) => S
266-
267-
export function subscribeWithSelector<
268-
S extends State,
269-
CustomSetState extends SetState<S>,
270-
CustomGetState extends GetState<S>,
271-
CustomStoreApi extends Omit<StoreApi<S>, 'subscribe'> & {
272-
subscribe: SubscribeWithSelector<S>
273-
subscribeWithSelectorEnabled: true
274-
}
275-
>(
276-
fn: (set: CustomSetState, get: CustomGetState, api: CustomStoreApi) => S
277-
): (set: CustomSetState, get: CustomGetState, api: CustomStoreApi) => S
278-
279-
export function subscribeWithSelector<
280-
S extends State,
281-
CustomSetState extends SetState<S>,
282-
CustomGetState extends GetState<S>,
283-
CustomStoreApi extends Omit<StoreApi<S>, 'subscribe'> & {
284-
subscribe: SubscribeWithSelector<S>
285-
subscribeWithSelectorEnabled: true
286-
}
287-
>(fn: (set: CustomSetState, get: CustomGetState, api: CustomStoreApi) => S) {
288-
return (set: CustomSetState, get: CustomGetState, api: CustomStoreApi): S => {
226+
export const subscribeWithSelector =
227+
<
228+
S extends State,
229+
CustomSetState extends SetState<S>,
230+
CustomGetState extends GetState<S>,
231+
CustomStoreApi extends StoreApiWithSubscribeWithSelector<S>
232+
>(
233+
fn: (set: CustomSetState, get: CustomGetState, api: CustomStoreApi) => S
234+
) =>
235+
(set: CustomSetState, get: CustomGetState, api: CustomStoreApi): S => {
289236
const origSubscribe = api.subscribe as Subscribe<S>
290237
api.subscribe = ((selector: any, optListener: any, options: any) => {
291238
let listener: StateListener<S> = selector // if no selector
@@ -304,65 +251,35 @@ export function subscribeWithSelector<
304251
}
305252
}
306253
return origSubscribe(listener)
307-
}) as SubscribeWithSelector<S>
254+
}) as any
255+
// Note: This will be removed in v4
308256
api.subscribeWithSelectorEnabled = true
309257
const initialState = fn(set, get, api)
310258
return initialState
311259
}
312-
}
313260

314261
type Combine<T, U> = Omit<T, keyof U> & U
315262

316-
export function combine<
317-
PrimaryState extends State,
318-
SecondaryState extends State
319-
>(
320-
initialState: PrimaryState,
321-
create: (
322-
set: NamedSet<PrimaryState>,
323-
get: GetState<PrimaryState>,
324-
api: StoreApi<PrimaryState>
325-
) => SecondaryState
326-
): (
327-
set: NamedSet<Combine<PrimaryState, SecondaryState>>,
328-
get: GetState<Combine<PrimaryState, SecondaryState>>,
329-
api: StoreApi<Combine<PrimaryState, SecondaryState>>
330-
) => Combine<PrimaryState, SecondaryState>
331-
332-
export function combine<
333-
PrimaryState extends State,
334-
SecondaryState extends State
335-
>(
336-
initialState: PrimaryState,
337-
create: (
338-
set: SetState<PrimaryState>,
339-
get: GetState<PrimaryState>,
340-
api: StoreApi<PrimaryState>
341-
) => SecondaryState
342-
): (
343-
set: SetState<Combine<PrimaryState, SecondaryState>>,
344-
get: GetState<Combine<PrimaryState, SecondaryState>>,
345-
api: StoreApi<Combine<PrimaryState, SecondaryState>>
346-
) => Combine<PrimaryState, SecondaryState>
347-
348-
export function combine<
349-
PrimaryState extends State,
350-
SecondaryState extends State
351-
>(
352-
initialState: PrimaryState,
353-
create: (
354-
set: SetState<PrimaryState>,
355-
get: GetState<PrimaryState>,
356-
api: StoreApi<PrimaryState>
357-
) => SecondaryState
358-
) {
359-
return (
263+
export const combine =
264+
<PrimaryState extends State, SecondaryState extends State>(
265+
initialState: PrimaryState,
266+
create: (
267+
// Note: NamedSet added for convenience
268+
set: SetState<PrimaryState> & NamedSet<PrimaryState>,
269+
get: GetState<PrimaryState>,
270+
api: StoreApi<PrimaryState>
271+
) => SecondaryState
272+
) =>
273+
(
360274
set: SetState<Combine<PrimaryState, SecondaryState>>,
361275
get: GetState<Combine<PrimaryState, SecondaryState>>,
362276
api: StoreApi<Combine<PrimaryState, SecondaryState>>
363277
) =>
364-
Object.assign({}, initialState, create(set as any, get as any, api as any))
365-
}
278+
Object.assign(
279+
{},
280+
initialState,
281+
create(set as any, get as any, api as any)
282+
) as Combine<PrimaryState, SecondaryState>
366283

367284
type DeepPartial<T extends Object> = {
368285
[P in keyof T]?: DeepPartial<T[P]>

tests/context.test.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import { Component as ClassComponent, useEffect, useState } from 'react'
22
import { render } from '@testing-library/react'
3-
import create from 'zustand'
3+
import create, { GetState, SetState } from 'zustand'
44
import createContext from 'zustand/context'
5-
import { subscribeWithSelector } from 'zustand/middleware'
5+
import {
6+
StoreApiWithSubscribeWithSelector,
7+
subscribeWithSelector,
8+
} from 'zustand/middleware'
69

710
const consoleError = console.error
811
afterEach(() => {
@@ -65,8 +68,13 @@ it('uses context store with selectors', async () => {
6568

6669
it('uses context store api', async () => {
6770
const createStore = () =>
68-
create(
69-
subscribeWithSelector<CounterState>((set) => ({
71+
create<
72+
CounterState,
73+
SetState<CounterState>,
74+
GetState<CounterState>,
75+
StoreApiWithSubscribeWithSelector<CounterState>
76+
>(
77+
subscribeWithSelector((set) => ({
7078
count: 0,
7179
inc: () => set((state) => ({ count: state.count + 1 })),
7280
}))

0 commit comments

Comments
 (0)