muya 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +0 -0
- package/README.md +236 -0
- package/cjs/index.js +1 -0
- package/esm/common.js +1 -0
- package/esm/create-base-state.js +1 -0
- package/esm/create-emitter.js +1 -0
- package/esm/create-getter-state.js +1 -0
- package/esm/create.js +1 -0
- package/esm/index.js +1 -0
- package/esm/is.js +1 -0
- package/esm/merge.js +1 -0
- package/esm/select.js +1 -0
- package/esm/shallow.js +1 -0
- package/esm/types.js +1 -0
- package/esm/use-state-value.js +1 -0
- package/package.json +37 -0
- package/src/common.ts +28 -0
- package/src/create-base-state.ts +35 -0
- package/src/create-emitter.ts +24 -0
- package/src/create-getter-state.ts +19 -0
- package/src/create.ts +102 -0
- package/src/index.ts +6 -0
- package/src/is.ts +36 -0
- package/src/merge.ts +41 -0
- package/src/select.ts +33 -0
- package/src/shallow.ts +58 -0
- package/src/state.test.tsx +647 -0
- package/src/types.ts +94 -0
- package/src/use-state-value.ts +29 -0
- package/types/common.d.ts +7 -0
- package/types/create-base-state.d.ts +10 -0
- package/types/create-emitter.d.ts +7 -0
- package/types/create-getter-state.d.ts +6 -0
- package/types/create.d.ts +21 -0
- package/types/index.d.ts +6 -0
- package/types/is.d.ts +10 -0
- package/types/merge.d.ts +2 -0
- package/types/select.d.ts +2 -0
- package/types/shallow.d.ts +1 -0
- package/types/types.d.ts +68 -0
- package/types/use-state-value.d.ts +10 -0
package/src/shallow.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { isArray, isMap, isSet } from './is'
|
|
2
|
+
|
|
3
|
+
// eslint-disable-next-line sonarjs/cognitive-complexity
|
|
4
|
+
export function shallow<T>(valueA: T, valueB: T): boolean {
|
|
5
|
+
if (valueA == valueB) {
|
|
6
|
+
return true
|
|
7
|
+
}
|
|
8
|
+
if (Object.is(valueA, valueB)) {
|
|
9
|
+
return true
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (typeof valueA !== 'object' || valueA == null || typeof valueB !== 'object' || valueB == null) {
|
|
13
|
+
return false
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (isMap(valueA) && isMap(valueB)) {
|
|
17
|
+
if (valueA.size !== valueB.size) return false
|
|
18
|
+
for (const [key, value] of valueA) {
|
|
19
|
+
if (!Object.is(value, valueB.get(key))) {
|
|
20
|
+
return false
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return true
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (isSet(valueA) && isSet(valueB)) {
|
|
27
|
+
if (valueA.size !== valueB.size) return false
|
|
28
|
+
for (const value of valueA) {
|
|
29
|
+
if (!valueB.has(value)) {
|
|
30
|
+
return false
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return true
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (isArray(valueA) && isArray(valueB)) {
|
|
37
|
+
if (valueA.length !== valueB.length) return false
|
|
38
|
+
for (const [index, element] of valueA.entries()) {
|
|
39
|
+
if (!Object.is(element, valueB[index])) {
|
|
40
|
+
return false
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return true
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const keysA = Object.keys(valueA as Record<string, unknown>)
|
|
47
|
+
const keysB = Object.keys(valueB as Record<string, unknown>)
|
|
48
|
+
if (keysA.length !== keysB.length) return false
|
|
49
|
+
for (const key of keysA) {
|
|
50
|
+
if (
|
|
51
|
+
!Object.prototype.hasOwnProperty.call(valueB, key) ||
|
|
52
|
+
!Object.is((valueA as Record<string, unknown>)[key], (valueB as Record<string, unknown>)[key])
|
|
53
|
+
) {
|
|
54
|
+
return false
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return true
|
|
58
|
+
}
|
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-shadow */
|
|
2
|
+
/* eslint-disable no-shadow */
|
|
3
|
+
import { Suspense } from 'react'
|
|
4
|
+
import { create } from './create'
|
|
5
|
+
import { renderHook, act, waitFor, render, screen } from '@testing-library/react'
|
|
6
|
+
import { shallow } from './shallow'
|
|
7
|
+
describe('state', () => {
|
|
8
|
+
it('should test state', () => {
|
|
9
|
+
const appState = create({ count: 0 })
|
|
10
|
+
expect(appState.getState()).toEqual({ count: 0 })
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('should render state with promise hook', async () => {
|
|
14
|
+
const promise = Promise.resolve({ count: 100 })
|
|
15
|
+
const appState = create(promise)
|
|
16
|
+
const renderCount = { current: 0 }
|
|
17
|
+
// const
|
|
18
|
+
|
|
19
|
+
const result = renderHook(() => {
|
|
20
|
+
renderCount.current++
|
|
21
|
+
return appState()
|
|
22
|
+
})
|
|
23
|
+
// wait for the promise to be resolved
|
|
24
|
+
await waitFor(() => {})
|
|
25
|
+
expect(result.result.current).toEqual({ count: 100 })
|
|
26
|
+
// count rendered
|
|
27
|
+
expect(renderCount.current).toEqual(2)
|
|
28
|
+
expect(appState.getState()).toEqual({ count: 100 })
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('should render state with get promise hook', async () => {
|
|
32
|
+
// eslint-disable-next-line unicorn/consistent-function-scoping
|
|
33
|
+
const getPromise = () => Promise.resolve({ count: 100 })
|
|
34
|
+
const appState = create(getPromise)
|
|
35
|
+
const renderCount = { current: 0 }
|
|
36
|
+
// const
|
|
37
|
+
|
|
38
|
+
const result = renderHook(() => {
|
|
39
|
+
renderCount.current++
|
|
40
|
+
return appState()
|
|
41
|
+
})
|
|
42
|
+
// wait for the promise to be resolved
|
|
43
|
+
await waitFor(() => {})
|
|
44
|
+
act(() => {
|
|
45
|
+
appState.setState({ count: 15 })
|
|
46
|
+
})
|
|
47
|
+
expect(result.result.current).toEqual({ count: 15 })
|
|
48
|
+
// count rendered
|
|
49
|
+
expect(renderCount.current).toEqual(3)
|
|
50
|
+
expect(appState.getState()).toEqual({ count: 15 })
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('should render state with get promise check default', async () => {
|
|
54
|
+
// eslint-disable-next-line unicorn/consistent-function-scoping
|
|
55
|
+
const getPromise = () => Promise.resolve({ count: 100 })
|
|
56
|
+
const appState = create(getPromise)
|
|
57
|
+
// const
|
|
58
|
+
|
|
59
|
+
// wait for the promise to be resolved
|
|
60
|
+
await waitFor(() => {})
|
|
61
|
+
act(() => {
|
|
62
|
+
appState.setState({ count: 15 })
|
|
63
|
+
})
|
|
64
|
+
expect(appState.getState()).toEqual({ count: 15 })
|
|
65
|
+
// count rendered
|
|
66
|
+
act(() => {
|
|
67
|
+
appState.reset()
|
|
68
|
+
})
|
|
69
|
+
expect(appState.getState()).toEqual({ count: 15 })
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('should render state with get hook', async () => {
|
|
73
|
+
// eslint-disable-next-line unicorn/consistent-function-scoping
|
|
74
|
+
const get = () => ({ count: 100 })
|
|
75
|
+
const appState = create(get)
|
|
76
|
+
const renderCount = { current: 0 }
|
|
77
|
+
// const
|
|
78
|
+
|
|
79
|
+
const result = renderHook(() => {
|
|
80
|
+
renderCount.current++
|
|
81
|
+
return appState()
|
|
82
|
+
})
|
|
83
|
+
// wait for the promise to be resolved
|
|
84
|
+
await waitFor(() => {})
|
|
85
|
+
act(() => {
|
|
86
|
+
appState.setState({ count: 15 })
|
|
87
|
+
})
|
|
88
|
+
expect(result.result.current).toEqual({ count: 15 })
|
|
89
|
+
// count rendered
|
|
90
|
+
expect(renderCount.current).toEqual(2)
|
|
91
|
+
expect(appState.getState()).toEqual({ count: 15 })
|
|
92
|
+
|
|
93
|
+
act(() => {
|
|
94
|
+
appState.reset()
|
|
95
|
+
})
|
|
96
|
+
expect(result.result.current).toEqual({ count: 100 })
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('should render state with get', async () => {
|
|
100
|
+
let wasCalled = false
|
|
101
|
+
const get = () => {
|
|
102
|
+
wasCalled = true
|
|
103
|
+
return { count: 100 }
|
|
104
|
+
}
|
|
105
|
+
const appState = create(get)
|
|
106
|
+
expect(wasCalled).toEqual(false)
|
|
107
|
+
appState.getState()
|
|
108
|
+
expect(wasCalled).toEqual(true)
|
|
109
|
+
})
|
|
110
|
+
it('should render state with get hook', async () => {
|
|
111
|
+
let wasCalled = false
|
|
112
|
+
const get = () => {
|
|
113
|
+
wasCalled = true
|
|
114
|
+
return { count: 100 }
|
|
115
|
+
}
|
|
116
|
+
const appState = create(get)
|
|
117
|
+
expect(wasCalled).toEqual(false)
|
|
118
|
+
renderHook(() => {
|
|
119
|
+
appState()
|
|
120
|
+
})
|
|
121
|
+
expect(wasCalled).toEqual(true)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('should render state with promise with suspense', async () => {
|
|
125
|
+
const promise = Promise.resolve({ count: 100 })
|
|
126
|
+
const appState = create(promise)
|
|
127
|
+
const renderCount = { current: 0 }
|
|
128
|
+
|
|
129
|
+
const MockedComponent = jest.fn(() => <div>loading</div>)
|
|
130
|
+
const MockedComponentAfterSuspense = jest.fn(() => <div>loaded</div>)
|
|
131
|
+
// const
|
|
132
|
+
function Component() {
|
|
133
|
+
renderCount.current++
|
|
134
|
+
return (
|
|
135
|
+
<div>
|
|
136
|
+
{appState().count}
|
|
137
|
+
<MockedComponentAfterSuspense />
|
|
138
|
+
</div>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
render(
|
|
142
|
+
<Suspense fallback={<MockedComponent />}>
|
|
143
|
+
<Component />
|
|
144
|
+
</Suspense>,
|
|
145
|
+
)
|
|
146
|
+
expect(MockedComponent).toHaveBeenCalledTimes(1)
|
|
147
|
+
expect(MockedComponentAfterSuspense).toHaveBeenCalledTimes(0)
|
|
148
|
+
await waitFor(() => {
|
|
149
|
+
return screen.getByText('100')
|
|
150
|
+
})
|
|
151
|
+
expect(MockedComponent).toHaveBeenCalledTimes(1)
|
|
152
|
+
expect(MockedComponentAfterSuspense).toHaveBeenCalledTimes(1)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('should render state', () => {
|
|
156
|
+
const appState = create({ count: 0 })
|
|
157
|
+
const renderCount = { current: 0 }
|
|
158
|
+
// const
|
|
159
|
+
|
|
160
|
+
const result = renderHook(() => {
|
|
161
|
+
renderCount.current++
|
|
162
|
+
return appState()
|
|
163
|
+
})
|
|
164
|
+
expect(result.result.current).toEqual({ count: 0 })
|
|
165
|
+
// count rendered
|
|
166
|
+
expect(renderCount.current).toEqual(1)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('should render state', () => {
|
|
170
|
+
const appState = create({ count: 0 })
|
|
171
|
+
const slice = appState.select((slice) => slice.count)
|
|
172
|
+
const renderCount = { current: 0 }
|
|
173
|
+
// const
|
|
174
|
+
|
|
175
|
+
const result = renderHook(() => {
|
|
176
|
+
renderCount.current++
|
|
177
|
+
return slice()
|
|
178
|
+
})
|
|
179
|
+
expect(result.result.current).toEqual(0)
|
|
180
|
+
// count rendered
|
|
181
|
+
expect(renderCount.current).toEqual(1)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('should render state with change', () => {
|
|
185
|
+
const appState = create({ count: 0 })
|
|
186
|
+
const renderCount = { current: 0 }
|
|
187
|
+
// const
|
|
188
|
+
|
|
189
|
+
const result = renderHook(() => {
|
|
190
|
+
renderCount.current++
|
|
191
|
+
return appState((slice) => slice)
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
act(() => {
|
|
195
|
+
appState.setState({ count: 1 })
|
|
196
|
+
})
|
|
197
|
+
expect(result.result.current).toEqual({ count: 1 })
|
|
198
|
+
expect(renderCount.current).toEqual(2)
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('should render state with slice change', () => {
|
|
202
|
+
const appState = create({ count: { nested: 0, array: [0] } })
|
|
203
|
+
const renderCount = { current: 0 }
|
|
204
|
+
const useNestedSlice = appState.select((slice) => slice.count)
|
|
205
|
+
const useNestedSliceArray = appState.select((slice) => slice.count.array.length)
|
|
206
|
+
const result = renderHook(() => {
|
|
207
|
+
return appState()
|
|
208
|
+
})
|
|
209
|
+
const sliceResult = renderHook(() => {
|
|
210
|
+
renderCount.current++
|
|
211
|
+
return useNestedSlice()
|
|
212
|
+
})
|
|
213
|
+
const sliceArrayResult = renderHook(() => {
|
|
214
|
+
return useNestedSliceArray()
|
|
215
|
+
})
|
|
216
|
+
expect(sliceArrayResult.result.current).toEqual(1)
|
|
217
|
+
expect(sliceResult.result.current).toEqual({ nested: 0, array: [0] })
|
|
218
|
+
act(() => {
|
|
219
|
+
appState.setState({ count: { nested: 2, array: [0] } })
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
expect(result.result.current).toEqual({ count: { nested: 2, array: [0] } })
|
|
223
|
+
expect(sliceResult.result.current).toEqual({ nested: 2, array: [0] })
|
|
224
|
+
|
|
225
|
+
act(() => {
|
|
226
|
+
appState.setState({ count: { nested: 2, array: [1, 2, 4] } })
|
|
227
|
+
})
|
|
228
|
+
expect(sliceArrayResult.result.current).toEqual(3)
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('should render multiple state', () => {
|
|
232
|
+
const mainState = create({ count: { nestedCount: 2 } })
|
|
233
|
+
const slice1 = mainState.select((slice) => slice.count)
|
|
234
|
+
const slice2FromSlice1 = slice1.select((slice) => slice.nestedCount)
|
|
235
|
+
|
|
236
|
+
const slice2FromSlice1Result = renderHook(() => slice2FromSlice1())
|
|
237
|
+
expect(slice2FromSlice1Result.result.current).toEqual(2)
|
|
238
|
+
|
|
239
|
+
act(() => {
|
|
240
|
+
mainState.setState({ count: { nestedCount: 3 } })
|
|
241
|
+
})
|
|
242
|
+
expect(slice2FromSlice1Result.result.current).toEqual(3)
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it('should render multiple state with change', () => {
|
|
246
|
+
const appState = create({ count: 0 })
|
|
247
|
+
const renderCount1 = { current: 0 }
|
|
248
|
+
const renderCount2 = { current: 0 }
|
|
249
|
+
// const
|
|
250
|
+
|
|
251
|
+
const result1 = renderHook(() => {
|
|
252
|
+
renderCount1.current++
|
|
253
|
+
return appState()
|
|
254
|
+
})
|
|
255
|
+
const result2 = renderHook(() => {
|
|
256
|
+
renderCount2.current++
|
|
257
|
+
return appState((slice) => slice.count)
|
|
258
|
+
})
|
|
259
|
+
act(() => {
|
|
260
|
+
appState.setState({ count: 1 })
|
|
261
|
+
})
|
|
262
|
+
expect(result1.result.current).toEqual({ count: 1 })
|
|
263
|
+
expect(result2.result.current).toEqual(1)
|
|
264
|
+
expect(renderCount1.current).toEqual(2)
|
|
265
|
+
expect(renderCount2.current).toEqual(2)
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
it('should test initial state', () => {
|
|
269
|
+
const appState = create({ count: 0 })
|
|
270
|
+
expect(appState.getState()).toEqual({ count: 0 })
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
it('should render initial state', () => {
|
|
274
|
+
const appState = create({ count: 0 })
|
|
275
|
+
const renderCount = { current: 0 }
|
|
276
|
+
|
|
277
|
+
const result = renderHook(() => {
|
|
278
|
+
renderCount.current++
|
|
279
|
+
return appState()
|
|
280
|
+
})
|
|
281
|
+
expect(result.result.current).toEqual({ count: 0 })
|
|
282
|
+
expect(renderCount.current).toEqual(1)
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
it('should render state after change', () => {
|
|
286
|
+
const appState = create({ count: 0 })
|
|
287
|
+
const renderCount = { current: 0 }
|
|
288
|
+
|
|
289
|
+
const result = renderHook(() => {
|
|
290
|
+
renderCount.current++
|
|
291
|
+
return appState((slice) => slice)
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
act(() => {
|
|
295
|
+
appState.setState({ count: 1 })
|
|
296
|
+
})
|
|
297
|
+
expect(result.result.current).toEqual({ count: 1 })
|
|
298
|
+
expect(renderCount.current).toEqual(2)
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
it('should render state with nested slice change', () => {
|
|
302
|
+
const appState = create({ count: { nested: 0, array: [0] } })
|
|
303
|
+
const renderCount = { current: 0 }
|
|
304
|
+
const useNestedSlice = appState.select((slice) => slice.count)
|
|
305
|
+
const useNestedSliceArray = appState.select((slice) => slice.count.array.length)
|
|
306
|
+
|
|
307
|
+
const result = renderHook(() => appState())
|
|
308
|
+
const sliceResult = renderHook(() => {
|
|
309
|
+
renderCount.current++
|
|
310
|
+
return useNestedSlice()
|
|
311
|
+
})
|
|
312
|
+
const sliceArrayResult = renderHook(() => useNestedSliceArray())
|
|
313
|
+
|
|
314
|
+
expect(sliceArrayResult.result.current).toEqual(1)
|
|
315
|
+
expect(sliceResult.result.current).toEqual({ nested: 0, array: [0] })
|
|
316
|
+
|
|
317
|
+
act(() => {
|
|
318
|
+
appState.setState({ count: { nested: 2, array: [0] } })
|
|
319
|
+
})
|
|
320
|
+
expect(result.result.current).toEqual({ count: { nested: 2, array: [0] } })
|
|
321
|
+
expect(sliceResult.result.current).toEqual({ nested: 2, array: [0] })
|
|
322
|
+
|
|
323
|
+
act(() => {
|
|
324
|
+
appState.setState({ count: { nested: 2, array: [1, 2, 4] } })
|
|
325
|
+
})
|
|
326
|
+
expect(sliceArrayResult.result.current).toEqual(3)
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
it('should render multiple state slices with updates', () => {
|
|
330
|
+
const mainState = create({ count: { nestedCount: 2 } })
|
|
331
|
+
const slice1 = mainState.select((slice) => slice.count)
|
|
332
|
+
const slice2FromSlice1 = slice1.select((slice) => slice.nestedCount)
|
|
333
|
+
|
|
334
|
+
const slice2FromSlice1Result = renderHook(() => slice2FromSlice1())
|
|
335
|
+
expect(slice2FromSlice1Result.result.current).toEqual(2)
|
|
336
|
+
|
|
337
|
+
act(() => {
|
|
338
|
+
mainState.setState({ count: { nestedCount: 3 } })
|
|
339
|
+
})
|
|
340
|
+
expect(slice2FromSlice1Result.result.current).toEqual(3)
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
it('should render multiple components observing the same state', () => {
|
|
344
|
+
const appState = create({ count: 0 })
|
|
345
|
+
const renderCount1 = { current: 0 }
|
|
346
|
+
const renderCount2 = { current: 0 }
|
|
347
|
+
|
|
348
|
+
const result1 = renderHook(() => {
|
|
349
|
+
renderCount1.current++
|
|
350
|
+
return appState()
|
|
351
|
+
})
|
|
352
|
+
const result2 = renderHook(() => {
|
|
353
|
+
renderCount2.current++
|
|
354
|
+
return appState((slice) => slice.count)
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
act(() => {
|
|
358
|
+
appState.setState({ count: 1 })
|
|
359
|
+
})
|
|
360
|
+
expect(result1.result.current).toEqual({ count: 1 })
|
|
361
|
+
expect(result2.result.current).toEqual(1)
|
|
362
|
+
expect(renderCount1.current).toEqual(2)
|
|
363
|
+
expect(renderCount2.current).toEqual(2)
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
it('should reset state to default value', () => {
|
|
367
|
+
const appState = create({ count: 0 })
|
|
368
|
+
act(() => {
|
|
369
|
+
appState.setState({ count: 10 })
|
|
370
|
+
})
|
|
371
|
+
expect(appState.getState()).toEqual({ count: 10 })
|
|
372
|
+
|
|
373
|
+
act(() => {
|
|
374
|
+
appState.reset()
|
|
375
|
+
})
|
|
376
|
+
expect(appState.getState()).toEqual({ count: 0 })
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
it('should handle updates with deep nesting in state', () => {
|
|
380
|
+
const appState = create({ data: { nested: { value: 1 } } })
|
|
381
|
+
const nestedSlice = appState.select((s) => s.data.nested.value)
|
|
382
|
+
|
|
383
|
+
const result = renderHook(() => nestedSlice())
|
|
384
|
+
expect(result.result.current).toEqual(1)
|
|
385
|
+
|
|
386
|
+
act(() => {
|
|
387
|
+
appState.setState({ data: { nested: { value: 2 } } })
|
|
388
|
+
})
|
|
389
|
+
expect(result.result.current).toEqual(2)
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
it('should not re-render for unrelated slice changes', () => {
|
|
393
|
+
const appState = create({ count: 0, unrelated: 5 })
|
|
394
|
+
const renderCount = { current: 0 }
|
|
395
|
+
|
|
396
|
+
const countSlice = appState.select((state) => state.count)
|
|
397
|
+
const unrelatedSlice = appState.select((state) => state.unrelated)
|
|
398
|
+
|
|
399
|
+
renderHook(() => {
|
|
400
|
+
renderCount.current++
|
|
401
|
+
return countSlice()
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
const unrelatedResult = renderHook(() => unrelatedSlice())
|
|
405
|
+
expect(renderCount.current).toEqual(1)
|
|
406
|
+
|
|
407
|
+
act(() => {
|
|
408
|
+
return appState.setState({ unrelated: 10 } as never)
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
expect(unrelatedResult.result.current).toEqual(10)
|
|
412
|
+
expect(renderCount.current).toEqual(2) // No re-render for count slice
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
it('should not re-render where isEqual return true on state', () => {
|
|
416
|
+
const appState = create({ count: 0 }, () => true)
|
|
417
|
+
const renderCount = { current: 0 }
|
|
418
|
+
|
|
419
|
+
renderHook(() => {
|
|
420
|
+
renderCount.current++
|
|
421
|
+
return appState()
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
act(() => {
|
|
425
|
+
appState.setState({ count: 10 })
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
expect(renderCount.current).toEqual(1)
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
it('should not re-render where isEqual return true hook slice', () => {
|
|
432
|
+
const appState = create({ count: 0 })
|
|
433
|
+
const renderCount = { current: 0 }
|
|
434
|
+
|
|
435
|
+
renderHook(() => {
|
|
436
|
+
renderCount.current++
|
|
437
|
+
return appState(
|
|
438
|
+
(slice) => slice,
|
|
439
|
+
() => true,
|
|
440
|
+
)
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
act(() => {
|
|
444
|
+
appState.setState({ count: 10 })
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
expect(renderCount.current).toEqual(1)
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
it('should not re-render where isEqual return true on slice', () => {
|
|
451
|
+
const appState = create({ count: 0 })
|
|
452
|
+
const appStateSlice = appState.select(
|
|
453
|
+
(slice) => slice.count,
|
|
454
|
+
() => true,
|
|
455
|
+
)
|
|
456
|
+
const renderCount = { current: 0 }
|
|
457
|
+
|
|
458
|
+
renderHook(() => {
|
|
459
|
+
renderCount.current++
|
|
460
|
+
return appStateSlice()
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
act(() => {
|
|
464
|
+
appState.setState({ count: 10 })
|
|
465
|
+
})
|
|
466
|
+
expect(renderCount.current).toEqual(1)
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
it('should not re-render where isEqual return true on nested slice', () => {
|
|
470
|
+
const appState = create({ count: { nested: { count: 0 } } })
|
|
471
|
+
const appStateSlice = appState.select((slice) => slice.count)
|
|
472
|
+
const nestedAppSlice = appStateSlice.select(
|
|
473
|
+
(slice) => slice.nested.count,
|
|
474
|
+
() => true,
|
|
475
|
+
)
|
|
476
|
+
const renderCount = { current: 0 }
|
|
477
|
+
|
|
478
|
+
renderHook(() => {
|
|
479
|
+
renderCount.current++
|
|
480
|
+
return nestedAppSlice()
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
act(() => {
|
|
484
|
+
appState.setState({ count: { nested: { count: 10 } } })
|
|
485
|
+
})
|
|
486
|
+
expect(renderCount.current).toEqual(1)
|
|
487
|
+
})
|
|
488
|
+
it('should use merge states', () => {
|
|
489
|
+
const state1 = create(3)
|
|
490
|
+
const state2 = create(2)
|
|
491
|
+
const mergedState = state1.merge(state2, (s1, s2) => s1 + s2)
|
|
492
|
+
const result = renderHook(() => mergedState())
|
|
493
|
+
expect(result.result.current).toEqual(5)
|
|
494
|
+
act(() => {
|
|
495
|
+
state1.setState(5)
|
|
496
|
+
})
|
|
497
|
+
expect(result.result.current).toEqual(7)
|
|
498
|
+
act(() => {
|
|
499
|
+
state2.setState(3)
|
|
500
|
+
})
|
|
501
|
+
expect(result.result.current).toEqual(8)
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
it('should use merge states nested', () => {
|
|
505
|
+
const useName = create(() => 'John')
|
|
506
|
+
const useAge = create(() => 30)
|
|
507
|
+
const useUser = useName.merge(useAge, (name, age) => ({ name, age }), shallow)
|
|
508
|
+
const result = renderHook(() => useUser())
|
|
509
|
+
expect(result.result.current).toEqual({ name: 'John', age: 30 })
|
|
510
|
+
|
|
511
|
+
act(() => {
|
|
512
|
+
useName.setState('Jane')
|
|
513
|
+
})
|
|
514
|
+
expect(result.result.current).toEqual({ name: 'Jane', age: 30 })
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
it('should use slice with new reference', () => {
|
|
518
|
+
const useName = create(() => 'John')
|
|
519
|
+
const useDifferentName = useName.select(
|
|
520
|
+
(name) => ({
|
|
521
|
+
name,
|
|
522
|
+
}),
|
|
523
|
+
shallow,
|
|
524
|
+
)
|
|
525
|
+
const result = renderHook(() => useDifferentName())
|
|
526
|
+
expect(result.result.current).toEqual({ name: 'John' })
|
|
527
|
+
act(() => {
|
|
528
|
+
useName.setState('Jane')
|
|
529
|
+
})
|
|
530
|
+
expect(result.result.current).toEqual({ name: 'Jane' })
|
|
531
|
+
})
|
|
532
|
+
it('should check if subscribe works', () => {
|
|
533
|
+
const appState = create({ count: 0 })
|
|
534
|
+
let count = 0
|
|
535
|
+
const unsubscribe = appState.subscribe((state) => {
|
|
536
|
+
expect(state).toEqual({ count })
|
|
537
|
+
count++
|
|
538
|
+
})
|
|
539
|
+
|
|
540
|
+
act(() => {
|
|
541
|
+
appState.setState({ count: 1 })
|
|
542
|
+
})
|
|
543
|
+
expect(count).toEqual(2)
|
|
544
|
+
|
|
545
|
+
unsubscribe()
|
|
546
|
+
act(() => {
|
|
547
|
+
appState.setState({ count: 2 })
|
|
548
|
+
})
|
|
549
|
+
expect(count).toEqual(2)
|
|
550
|
+
})
|
|
551
|
+
|
|
552
|
+
it('should handle rapid consecutive state updates', () => {
|
|
553
|
+
const appState = create({ count: 0 })
|
|
554
|
+
const renderCount = { current: 0 }
|
|
555
|
+
|
|
556
|
+
const result = renderHook(() => {
|
|
557
|
+
renderCount.current++
|
|
558
|
+
return appState()
|
|
559
|
+
})
|
|
560
|
+
|
|
561
|
+
act(() => {
|
|
562
|
+
// batch updates
|
|
563
|
+
appState.setState({ count: 1 })
|
|
564
|
+
appState.setState({ count: 2 })
|
|
565
|
+
appState.setState({ count: 3 })
|
|
566
|
+
})
|
|
567
|
+
|
|
568
|
+
expect(result.result.current).toEqual({ count: 3 })
|
|
569
|
+
expect(renderCount.current).toEqual(2) // it's batch
|
|
570
|
+
})
|
|
571
|
+
|
|
572
|
+
it('should handle setting state to the same value', () => {
|
|
573
|
+
const appState = create({ count: 0 })
|
|
574
|
+
const renderCount = { current: 0 }
|
|
575
|
+
|
|
576
|
+
const result = renderHook(() => {
|
|
577
|
+
renderCount.current++
|
|
578
|
+
return appState()
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
act(() => {
|
|
582
|
+
appState.setState((previous) => previous)
|
|
583
|
+
})
|
|
584
|
+
|
|
585
|
+
expect(result.result.current).toEqual({ count: 0 })
|
|
586
|
+
expect(renderCount.current).toEqual(1)
|
|
587
|
+
})
|
|
588
|
+
|
|
589
|
+
it('should handle setting state with partial updates', () => {
|
|
590
|
+
const appState = create({ count: 0, name: 'John' })
|
|
591
|
+
const renderCount = { current: 0 }
|
|
592
|
+
|
|
593
|
+
const result = renderHook(() => {
|
|
594
|
+
renderCount.current++
|
|
595
|
+
return appState()
|
|
596
|
+
})
|
|
597
|
+
|
|
598
|
+
act(() => {
|
|
599
|
+
appState.updateState({ count: 1 })
|
|
600
|
+
})
|
|
601
|
+
|
|
602
|
+
expect(result.result.current).toEqual({ count: 1, name: 'John' })
|
|
603
|
+
expect(renderCount.current).toEqual(2)
|
|
604
|
+
})
|
|
605
|
+
|
|
606
|
+
it('should handle resetting state after multiple updates', () => {
|
|
607
|
+
const appState = create({ count: 0 })
|
|
608
|
+
const renderCount = { current: 0 }
|
|
609
|
+
|
|
610
|
+
const result = renderHook(() => {
|
|
611
|
+
renderCount.current++
|
|
612
|
+
return appState()
|
|
613
|
+
})
|
|
614
|
+
|
|
615
|
+
act(() => {
|
|
616
|
+
appState.setState({ count: 1 })
|
|
617
|
+
appState.setState({ count: 2 })
|
|
618
|
+
appState.reset()
|
|
619
|
+
})
|
|
620
|
+
|
|
621
|
+
expect(result.result.current).toEqual({ count: 0 })
|
|
622
|
+
expect(renderCount.current).toEqual(2)
|
|
623
|
+
})
|
|
624
|
+
|
|
625
|
+
it('should handle concurrent asynchronous state updates', async () => {
|
|
626
|
+
const appState = create({ count: 0 })
|
|
627
|
+
const renderCount = { current: 0 }
|
|
628
|
+
|
|
629
|
+
const result = renderHook(() => {
|
|
630
|
+
renderCount.current++
|
|
631
|
+
return appState()
|
|
632
|
+
})
|
|
633
|
+
|
|
634
|
+
act(() => {
|
|
635
|
+
// Simulate concurrent asynchronous updates
|
|
636
|
+
appState.setState({ count: 1 })
|
|
637
|
+
appState.setState({ count: 2 })
|
|
638
|
+
appState.setState({ count: 3 })
|
|
639
|
+
})
|
|
640
|
+
|
|
641
|
+
await waitFor(() => {
|
|
642
|
+
expect(result.result.current).toEqual({ count: 3 })
|
|
643
|
+
})
|
|
644
|
+
|
|
645
|
+
expect(renderCount.current).toBe(2)
|
|
646
|
+
})
|
|
647
|
+
})
|