muya 2.0.0-beta.2 → 2.0.0
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/README.md +124 -195
- package/cjs/index.js +1 -1
- package/esm/create-state.js +1 -0
- package/esm/create.js +1 -1
- package/esm/debug/development-tools.js +1 -1
- package/esm/index.js +1 -1
- package/esm/scheduler.js +1 -0
- package/esm/select.js +1 -0
- package/esm/use-value.js +1 -0
- package/esm/utils/__tests__/is.test.js +1 -1
- package/esm/utils/common.js +1 -1
- package/esm/utils/is.js +1 -1
- package/package.json +12 -12
- package/{packages/core → src}/__tests__/bench.test.tsx +3 -108
- package/src/__tests__/create.test.tsx +159 -0
- package/src/__tests__/scheduler.test.tsx +52 -0
- package/src/__tests__/select.test.tsx +127 -0
- package/src/__tests__/use-value.test.tsx +78 -0
- package/src/create-state.ts +50 -0
- package/src/create.ts +67 -0
- package/{packages/core → src}/debug/development-tools.ts +18 -3
- package/{packages/core → src}/index.ts +2 -1
- package/{packages/core/utils/global-scheduler.ts → src/scheduler.ts} +9 -3
- package/src/select.ts +69 -0
- package/src/types.ts +66 -0
- package/src/use-value.ts +22 -0
- package/{packages/core → src}/utils/__tests__/is.test.ts +24 -7
- package/{packages/core → src}/utils/common.ts +35 -10
- package/{packages/core → src}/utils/is.ts +5 -8
- package/types/create-state.d.ts +12 -0
- package/types/create.d.ts +6 -18
- package/types/debug/development-tools.d.ts +2 -9
- package/types/index.d.ts +2 -1
- package/types/{utils/scheduler.d.ts → scheduler.d.ts} +4 -1
- package/types/select.d.ts +10 -0
- package/types/types.d.ts +55 -5
- package/types/use-value.d.ts +2 -0
- package/types/utils/common.d.ts +6 -5
- package/types/utils/is.d.ts +3 -4
- package/esm/__tests__/create-async.test.js +0 -1
- package/esm/subscriber.js +0 -1
- package/esm/use.js +0 -1
- package/esm/utils/__tests__/context.test.js +0 -1
- package/esm/utils/__tests__/sub-memo.test.js +0 -1
- package/esm/utils/create-context.js +0 -1
- package/esm/utils/global-scheduler.js +0 -1
- package/esm/utils/scheduler.js +0 -1
- package/esm/utils/sub-memo.js +0 -1
- package/packages/core/__tests__/create-async.test.ts +0 -88
- package/packages/core/__tests__/create.test.tsx +0 -107
- package/packages/core/__tests__/subscriber.test.tsx +0 -89
- package/packages/core/__tests__/use-async.test.tsx +0 -45
- package/packages/core/__tests__/use.test.tsx +0 -125
- package/packages/core/create.ts +0 -98
- package/packages/core/subscriber.ts +0 -165
- package/packages/core/types.ts +0 -15
- package/packages/core/use.ts +0 -57
- package/packages/core/utils/__tests__/context.test.ts +0 -198
- package/packages/core/utils/__tests__/sub-memo.test.ts +0 -13
- package/packages/core/utils/create-context.ts +0 -60
- package/packages/core/utils/scheduler.ts +0 -59
- package/packages/core/utils/sub-memo.ts +0 -49
- package/types/subscriber.d.ts +0 -25
- package/types/use.d.ts +0 -2
- package/types/utils/create-context.d.ts +0 -5
- package/types/utils/global-scheduler.d.ts +0 -5
- package/types/utils/sub-memo.d.ts +0 -7
- /package/{packages/core → src}/__tests__/test-utils.ts +0 -0
- /package/{packages/core → src}/utils/__tests__/shallow.test.ts +0 -0
- /package/{packages/core → src}/utils/create-emitter.ts +0 -0
- /package/{packages/core → src}/utils/shallow.ts +0 -0
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { act, renderHook } from '@testing-library/react-hooks'
|
|
9
9
|
import { useStore, create as zustand } from 'zustand'
|
|
10
10
|
import { useEffect, useState } from 'react'
|
|
11
|
-
import {
|
|
11
|
+
import { useValue } from '../use-value'
|
|
12
12
|
import { atom, useAtom } from 'jotai'
|
|
13
13
|
import { create } from '../create'
|
|
14
14
|
|
|
@@ -48,7 +48,7 @@ describe('benchmarks comparison measure', () => {
|
|
|
48
48
|
const { result, resolvePromise } = renderPerfHook(
|
|
49
49
|
() => {
|
|
50
50
|
reRendersBefore()
|
|
51
|
-
return
|
|
51
|
+
return useValue(state)
|
|
52
52
|
},
|
|
53
53
|
(data) => data,
|
|
54
54
|
count - 1,
|
|
@@ -139,7 +139,7 @@ describe('benchmarks comparison measure', () => {
|
|
|
139
139
|
const { result, resolvePromise } = renderPerfHook(
|
|
140
140
|
() => {
|
|
141
141
|
reRendersBefore()
|
|
142
|
-
return
|
|
142
|
+
return useValue(state)
|
|
143
143
|
},
|
|
144
144
|
(data) => data,
|
|
145
145
|
count - 1,
|
|
@@ -159,108 +159,3 @@ describe('benchmarks comparison measure', () => {
|
|
|
159
159
|
})
|
|
160
160
|
}
|
|
161
161
|
})
|
|
162
|
-
|
|
163
|
-
describe('benchmarks comparison between others', () => {
|
|
164
|
-
const reRendersBefore = jest.fn()
|
|
165
|
-
|
|
166
|
-
beforeEach(() => {
|
|
167
|
-
jest.clearAllMocks()
|
|
168
|
-
})
|
|
169
|
-
// const count = 10000
|
|
170
|
-
|
|
171
|
-
const counts = [100]
|
|
172
|
-
for (const count of counts) {
|
|
173
|
-
describe(`Count ${count}`, () => {
|
|
174
|
-
it(`should benchmark jotai ${count}`, async () => {
|
|
175
|
-
const state = atom(1)
|
|
176
|
-
const start = performance.now()
|
|
177
|
-
const { result, waitFor } = renderHook(() => {
|
|
178
|
-
reRendersBefore()
|
|
179
|
-
return useAtom(state)
|
|
180
|
-
})
|
|
181
|
-
|
|
182
|
-
for (let index = 0; index < count; index++) {
|
|
183
|
-
act(() => {
|
|
184
|
-
result.current[1](index)
|
|
185
|
-
})
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
await waitFor(() => {
|
|
189
|
-
expect(result.current[0]).toBe(count - 1)
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
const end = performance.now()
|
|
193
|
-
console.log('Time', end - start)
|
|
194
|
-
console.log('Renders', reRendersBefore.mock.calls.length)
|
|
195
|
-
// expect(reRendersBefore).toHaveBeenCalledTimes(3)
|
|
196
|
-
})
|
|
197
|
-
it(`should benchmark zustand ${count}`, async () => {
|
|
198
|
-
const state = zustand((_set) => ({ state: 1 }))
|
|
199
|
-
const start = performance.now()
|
|
200
|
-
const { result, waitFor } = renderHook(() => {
|
|
201
|
-
reRendersBefore()
|
|
202
|
-
return useStore(state)
|
|
203
|
-
})
|
|
204
|
-
|
|
205
|
-
for (let index = 0; index < count; index++) {
|
|
206
|
-
act(() => {
|
|
207
|
-
state.setState(index)
|
|
208
|
-
})
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
await waitFor(() => {
|
|
212
|
-
expect(result.current).toBe(count - 1)
|
|
213
|
-
})
|
|
214
|
-
|
|
215
|
-
const end = performance.now()
|
|
216
|
-
console.log('Time', end - start)
|
|
217
|
-
console.log('Renders', reRendersBefore.mock.calls.length)
|
|
218
|
-
})
|
|
219
|
-
|
|
220
|
-
it(`should benchmark react ${count}`, async () => {
|
|
221
|
-
const start = performance.now()
|
|
222
|
-
const { result, waitFor } = renderHook(() => {
|
|
223
|
-
reRendersBefore()
|
|
224
|
-
return useState(1)
|
|
225
|
-
})
|
|
226
|
-
|
|
227
|
-
for (let index = 0; index < count; index++) {
|
|
228
|
-
act(() => {
|
|
229
|
-
result.current[1](index)
|
|
230
|
-
})
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
await waitFor(() => {
|
|
234
|
-
expect(result.current[0]).toBe(count - 1)
|
|
235
|
-
})
|
|
236
|
-
|
|
237
|
-
const end = performance.now()
|
|
238
|
-
console.log('Time', end - start)
|
|
239
|
-
console.log('Renders', reRendersBefore.mock.calls.length)
|
|
240
|
-
})
|
|
241
|
-
})
|
|
242
|
-
it(`should benchmark ${count} muya`, async () => {
|
|
243
|
-
const state = create(1)
|
|
244
|
-
const start = performance.now()
|
|
245
|
-
const { result, waitFor } = renderHook(() => {
|
|
246
|
-
reRendersBefore()
|
|
247
|
-
return use(state)
|
|
248
|
-
})
|
|
249
|
-
|
|
250
|
-
for (let index = 0; index < count; index++) {
|
|
251
|
-
act(() => {
|
|
252
|
-
state.set(index)
|
|
253
|
-
})
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
await waitFor(() => {
|
|
257
|
-
expect(result.current).toBe(count - 1)
|
|
258
|
-
})
|
|
259
|
-
|
|
260
|
-
const end = performance.now()
|
|
261
|
-
console.log('Time', end - start)
|
|
262
|
-
console.log('Renders', reRendersBefore.mock.calls.length)
|
|
263
|
-
// expect(reRendersBefore).toHaveBeenCalledTimes(2)
|
|
264
|
-
})
|
|
265
|
-
}
|
|
266
|
-
})
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { create } from '../create'
|
|
2
|
+
import { waitFor } from '@testing-library/react'
|
|
3
|
+
import { longPromise } from './test-utils'
|
|
4
|
+
|
|
5
|
+
describe('create', () => {
|
|
6
|
+
it('should get basic value states', async () => {
|
|
7
|
+
const state1 = create(1)
|
|
8
|
+
const state2 = create(2)
|
|
9
|
+
expect(state1.get()).toBe(1)
|
|
10
|
+
expect(state2.get()).toBe(2)
|
|
11
|
+
|
|
12
|
+
state1.set(2)
|
|
13
|
+
state2.set(3)
|
|
14
|
+
|
|
15
|
+
await waitFor(() => {
|
|
16
|
+
expect(state1.get()).toBe(2)
|
|
17
|
+
expect(state2.get()).toBe(3)
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
it('should check if value is subscribed to the state', async () => {
|
|
21
|
+
const state = create(1)
|
|
22
|
+
const listener = jest.fn()
|
|
23
|
+
state.listen(listener)
|
|
24
|
+
state.set(2)
|
|
25
|
+
await waitFor(() => {
|
|
26
|
+
expect(listener).toHaveBeenCalledWith(2)
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('should check if value is unsubscribed from the state', async () => {
|
|
31
|
+
const state = create(1)
|
|
32
|
+
const listener = jest.fn()
|
|
33
|
+
const unsubscribe = state.listen(listener)
|
|
34
|
+
unsubscribe()
|
|
35
|
+
state.set(2)
|
|
36
|
+
await waitFor(() => {
|
|
37
|
+
expect(listener).not.toHaveBeenCalled()
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
it('should check change part of state, but is not equal', async () => {
|
|
41
|
+
const state = create({ count: 1, anotherCount: 1 }, (previous, next) => previous.anotherCount === next.anotherCount)
|
|
42
|
+
const listener = jest.fn()
|
|
43
|
+
state.listen(listener)
|
|
44
|
+
state.set((previous) => ({ ...previous, count: previous.count + 1 }))
|
|
45
|
+
await waitFor(() => {
|
|
46
|
+
expect(listener).not.toHaveBeenCalled()
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
it('should check change part of state, is not equal', async () => {
|
|
50
|
+
const state = create({ count: 1, anotherCount: 1 }, (previous, next) => previous.count === next.count)
|
|
51
|
+
const listener = jest.fn()
|
|
52
|
+
state.listen(listener)
|
|
53
|
+
state.set((previous) => ({ ...previous, count: previous.count + 1 }))
|
|
54
|
+
await waitFor(() => {
|
|
55
|
+
expect(listener).toHaveBeenCalledWith({ count: 2, anotherCount: 1 })
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('should initialize state with a function', () => {
|
|
60
|
+
const initialValue = jest.fn(() => 10)
|
|
61
|
+
const state = create(initialValue)
|
|
62
|
+
expect(initialValue).toHaveBeenCalled()
|
|
63
|
+
expect(state.get()).toBe(10)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('should handle asynchronous state updates', async () => {
|
|
67
|
+
const state = create(0)
|
|
68
|
+
const listener = jest.fn()
|
|
69
|
+
state.listen(listener)
|
|
70
|
+
setTimeout(() => {
|
|
71
|
+
state.set(1)
|
|
72
|
+
}, 100)
|
|
73
|
+
await waitFor(() => {
|
|
74
|
+
expect(state.get()).toBe(1)
|
|
75
|
+
expect(listener).toHaveBeenCalledWith(1)
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('should notify multiple listeners', async () => {
|
|
80
|
+
const state = create('initial')
|
|
81
|
+
const listener1 = jest.fn()
|
|
82
|
+
const listener2 = jest.fn()
|
|
83
|
+
state.listen(listener1)
|
|
84
|
+
state.listen(listener2)
|
|
85
|
+
state.set('updated')
|
|
86
|
+
await waitFor(() => {
|
|
87
|
+
expect(listener1).toHaveBeenCalledWith('updated')
|
|
88
|
+
expect(listener2).toHaveBeenCalledWith('updated')
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('should not update if isEqual returns true', async () => {
|
|
93
|
+
const state = create(1, () => true)
|
|
94
|
+
const listener = jest.fn()
|
|
95
|
+
state.listen(listener)
|
|
96
|
+
state.set(2)
|
|
97
|
+
await waitFor(() => {
|
|
98
|
+
expect(listener).not.toHaveBeenCalled()
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('should clear state and listeners on destroy', async () => {
|
|
103
|
+
const state = create(1)
|
|
104
|
+
const listener = jest.fn()
|
|
105
|
+
state.listen(listener)
|
|
106
|
+
state.destroy()
|
|
107
|
+
state.set(2)
|
|
108
|
+
await waitFor(() => {})
|
|
109
|
+
expect(state.get()).toBe(1)
|
|
110
|
+
expect(listener).not.toHaveBeenCalledWith(2)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('should create new get select state', async () => {
|
|
114
|
+
const state = create({ count: 1 })
|
|
115
|
+
const select = state.select((slice) => slice.count)
|
|
116
|
+
expect(select.get()).toBe(1)
|
|
117
|
+
|
|
118
|
+
state.set({ count: 2 })
|
|
119
|
+
await waitFor(() => {
|
|
120
|
+
expect(select.get()).toBe(2)
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('should create state with async value', async () => {
|
|
125
|
+
const state = create(() => longPromise(100))
|
|
126
|
+
await waitFor(() => {
|
|
127
|
+
expect(state.get()).toBe(0)
|
|
128
|
+
})
|
|
129
|
+
state.set(1)
|
|
130
|
+
await waitFor(() => {
|
|
131
|
+
expect(state.get()).toBe(1)
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
it('should create state with async value but will be cancelled by set value before it will resolve', async () => {
|
|
135
|
+
const state = create(() => longPromise(100))
|
|
136
|
+
state.set(2)
|
|
137
|
+
await waitFor(() => {
|
|
138
|
+
expect(state.get()).toBe(2)
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
it('should handle async select', async () => {
|
|
142
|
+
const state = create(0)
|
|
143
|
+
const asyncState = state.select(async (s) => {
|
|
144
|
+
await longPromise(100)
|
|
145
|
+
return s + 1
|
|
146
|
+
})
|
|
147
|
+
const listener = jest.fn()
|
|
148
|
+
asyncState.listen(listener)
|
|
149
|
+
await waitFor(() => {
|
|
150
|
+
expect(asyncState.get()).toBe(1)
|
|
151
|
+
expect(listener).toHaveBeenCalledWith(1)
|
|
152
|
+
})
|
|
153
|
+
state.set(1)
|
|
154
|
+
await waitFor(() => {
|
|
155
|
+
expect(asyncState.get()).toBe(2)
|
|
156
|
+
expect(listener).toHaveBeenCalledWith(2)
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
})
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { waitFor } from '@testing-library/react'
|
|
2
|
+
import { createScheduler } from '../scheduler'
|
|
3
|
+
|
|
4
|
+
describe('scheduler', () => {
|
|
5
|
+
it('should test scheduler by id', async () => {
|
|
6
|
+
const scheduler = createScheduler()
|
|
7
|
+
|
|
8
|
+
const id = 1
|
|
9
|
+
const value = 2
|
|
10
|
+
const callback = jest.fn()
|
|
11
|
+
scheduler.add(id, {
|
|
12
|
+
onFinish: callback,
|
|
13
|
+
})
|
|
14
|
+
scheduler.schedule(id, value)
|
|
15
|
+
await waitFor(() => {
|
|
16
|
+
expect(callback).toHaveBeenCalled()
|
|
17
|
+
})
|
|
18
|
+
})
|
|
19
|
+
it('should test scheduler with multiple ids', async () => {
|
|
20
|
+
const ids = [1, 2, 3]
|
|
21
|
+
const scheduler = createScheduler()
|
|
22
|
+
const callbacks: unknown[] = []
|
|
23
|
+
for (const id of ids) {
|
|
24
|
+
const callback = jest.fn()
|
|
25
|
+
scheduler.add(id, {
|
|
26
|
+
onFinish: callback,
|
|
27
|
+
})
|
|
28
|
+
callbacks.push(callback)
|
|
29
|
+
}
|
|
30
|
+
scheduler.schedule(1, 2)
|
|
31
|
+
await waitFor(() => {
|
|
32
|
+
expect(callbacks[0]).toHaveBeenCalled()
|
|
33
|
+
expect(callbacks[1]).not.toHaveBeenCalled()
|
|
34
|
+
expect(callbacks[2]).not.toHaveBeenCalled()
|
|
35
|
+
})
|
|
36
|
+
jest.clearAllMocks()
|
|
37
|
+
scheduler.schedule(2, 2)
|
|
38
|
+
await waitFor(() => {
|
|
39
|
+
expect(callbacks[0]).not.toHaveBeenCalled()
|
|
40
|
+
expect(callbacks[1]).toHaveBeenCalled()
|
|
41
|
+
expect(callbacks[2]).not.toHaveBeenCalled()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
jest.clearAllMocks()
|
|
45
|
+
scheduler.schedule(3, 2)
|
|
46
|
+
await waitFor(() => {
|
|
47
|
+
expect(callbacks[0]).not.toHaveBeenCalled()
|
|
48
|
+
expect(callbacks[1]).not.toHaveBeenCalled()
|
|
49
|
+
expect(callbacks[2]).toHaveBeenCalled()
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
})
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { create } from '../create'
|
|
2
|
+
import { select } from '../select'
|
|
3
|
+
import { waitFor } from '@testing-library/react'
|
|
4
|
+
import { longPromise } from './test-utils'
|
|
5
|
+
|
|
6
|
+
describe('select', () => {
|
|
7
|
+
it('should derive state from a single dependency', async () => {
|
|
8
|
+
const state = create(1)
|
|
9
|
+
const selectedState = select([state], (value) => value * 2)
|
|
10
|
+
expect(selectedState.get()).toBe(2)
|
|
11
|
+
state.set(2)
|
|
12
|
+
await waitFor(() => {})
|
|
13
|
+
expect(selectedState.get()).toBe(4)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('should derive state from multiple dependencies', async () => {
|
|
17
|
+
const state1 = create(1)
|
|
18
|
+
const state2 = create(2)
|
|
19
|
+
const selectedState = select([state1, state2], (a, b) => a + b)
|
|
20
|
+
expect(selectedState.get()).toBe(3)
|
|
21
|
+
state1.set(2)
|
|
22
|
+
await waitFor(() => {})
|
|
23
|
+
expect(selectedState.get()).toBe(4)
|
|
24
|
+
state2.set(3)
|
|
25
|
+
await waitFor(() => {})
|
|
26
|
+
expect(selectedState.get()).toBe(5)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('should notify listeners when derived state changes', async () => {
|
|
30
|
+
const state = create(1)
|
|
31
|
+
const selectedState = select([state], (value) => value * 2)
|
|
32
|
+
const listener = jest.fn()
|
|
33
|
+
selectedState.listen(listener)
|
|
34
|
+
state.set(2)
|
|
35
|
+
await waitFor(() => {
|
|
36
|
+
expect(selectedState.get()).toBe(4)
|
|
37
|
+
expect(listener).toHaveBeenCalledWith(4)
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('should not notify listeners if isEqual returns true', async () => {
|
|
42
|
+
const state = create(1)
|
|
43
|
+
const selectedState = select(
|
|
44
|
+
[state],
|
|
45
|
+
(value) => value * 2,
|
|
46
|
+
() => true,
|
|
47
|
+
)
|
|
48
|
+
const listener = jest.fn()
|
|
49
|
+
selectedState.listen(listener)
|
|
50
|
+
state.set(2)
|
|
51
|
+
await waitFor(() => {
|
|
52
|
+
expect(listener).not.toHaveBeenCalled()
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('should destroy select state properly', async () => {
|
|
57
|
+
const state = create(1)
|
|
58
|
+
const selectedState = select([state], (value) => value * 2)
|
|
59
|
+
const listener = jest.fn()
|
|
60
|
+
selectedState.listen(listener)
|
|
61
|
+
selectedState.destroy()
|
|
62
|
+
state.set(2)
|
|
63
|
+
await waitFor(() => {})
|
|
64
|
+
// there are no listeners to notify, so it return 4 as value is computed again, but internally it's destroyed and undefined
|
|
65
|
+
// so it works as expected
|
|
66
|
+
expect(selectedState.get()).toBe(4)
|
|
67
|
+
expect(listener).not.toHaveBeenCalled()
|
|
68
|
+
})
|
|
69
|
+
it('should handle async updates', async () => {
|
|
70
|
+
const state1 = create(1)
|
|
71
|
+
const state2 = create(2)
|
|
72
|
+
const selectedState = select([state1, state2], async (a, b) => {
|
|
73
|
+
await longPromise()
|
|
74
|
+
return a + b
|
|
75
|
+
})
|
|
76
|
+
const listener = jest.fn()
|
|
77
|
+
selectedState.listen(listener)
|
|
78
|
+
state1.set(2)
|
|
79
|
+
state2.set(3)
|
|
80
|
+
await waitFor(() => {
|
|
81
|
+
expect(selectedState.get()).toBe(5)
|
|
82
|
+
expect(listener).toHaveBeenCalledWith(5)
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
it('should handle async updates with async state', async () => {
|
|
86
|
+
const state = create(longPromise(100))
|
|
87
|
+
const selectedState = select([state], async (value) => {
|
|
88
|
+
await longPromise(100)
|
|
89
|
+
return (await value) + 1
|
|
90
|
+
})
|
|
91
|
+
const listener = jest.fn()
|
|
92
|
+
selectedState.listen(listener)
|
|
93
|
+
await waitFor(() => {
|
|
94
|
+
expect(selectedState.get()).toBe(1)
|
|
95
|
+
expect(listener).toHaveBeenCalledWith(1)
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
it('should handle sync state updates when one of par is changed', async () => {
|
|
99
|
+
const state1Atom = create(0)
|
|
100
|
+
const state2Atom = create(0)
|
|
101
|
+
const state3Atom = create(0)
|
|
102
|
+
|
|
103
|
+
const sumState = select([state1Atom, state2Atom, state3Atom], (a, b, c) => a + b + c)
|
|
104
|
+
|
|
105
|
+
const listener = jest.fn()
|
|
106
|
+
sumState.listen(listener)
|
|
107
|
+
expect(sumState.get()).toBe(0)
|
|
108
|
+
|
|
109
|
+
state1Atom.set(1)
|
|
110
|
+
await waitFor(() => {
|
|
111
|
+
expect(sumState.get()).toBe(1)
|
|
112
|
+
expect(listener).toHaveBeenCalledWith(1)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
state2Atom.set(1)
|
|
116
|
+
await waitFor(() => {
|
|
117
|
+
expect(sumState.get()).toBe(2)
|
|
118
|
+
expect(listener).toHaveBeenCalledWith(2)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
state3Atom.set(1)
|
|
122
|
+
await waitFor(() => {
|
|
123
|
+
expect(sumState.get()).toBe(3)
|
|
124
|
+
expect(listener).toHaveBeenCalledWith(3)
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
})
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { renderHook, act } from '@testing-library/react-hooks'
|
|
2
|
+
import { create } from '../create'
|
|
3
|
+
import { useValue } from '../use-value'
|
|
4
|
+
import { waitFor } from '@testing-library/react'
|
|
5
|
+
|
|
6
|
+
describe('useValue', () => {
|
|
7
|
+
it('should get the initial state value', () => {
|
|
8
|
+
const state = create(1)
|
|
9
|
+
const { result } = renderHook(() => useValue(state))
|
|
10
|
+
expect(result.current).toBe(1)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('should get the initial state value', () => {
|
|
14
|
+
const state = create(1)
|
|
15
|
+
const { result } = renderHook(() => state())
|
|
16
|
+
expect(result.current).toBe(1)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('should update when the state changes', async () => {
|
|
20
|
+
const state = create(1)
|
|
21
|
+
const { result } = renderHook(() => useValue(state))
|
|
22
|
+
act(() => {
|
|
23
|
+
state.set(2)
|
|
24
|
+
})
|
|
25
|
+
await waitFor(() => {
|
|
26
|
+
expect(result.current).toBe(2)
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('should use a selector function', () => {
|
|
31
|
+
const state = create({ count: 1 })
|
|
32
|
+
const { result } = renderHook(() => useValue(state, (s) => s.count))
|
|
33
|
+
expect(result.current).toBe(1)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('should handle errors thrown from state', () => {
|
|
37
|
+
const error = new Error('Test error')
|
|
38
|
+
const state = create(() => {
|
|
39
|
+
throw error
|
|
40
|
+
})
|
|
41
|
+
const { result } = renderHook(() => useValue(state))
|
|
42
|
+
expect(result.error).toBe(error)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('should handle promises returned from state suspense', async () => {
|
|
46
|
+
const promise = Promise.resolve(1)
|
|
47
|
+
const state = create(() => promise)
|
|
48
|
+
const renders = jest.fn()
|
|
49
|
+
const { result } = renderHook(() => {
|
|
50
|
+
renders()
|
|
51
|
+
return useValue(state)
|
|
52
|
+
})
|
|
53
|
+
await waitFor(() => {})
|
|
54
|
+
expect(result.current).toBe(1)
|
|
55
|
+
expect(renders).toHaveBeenCalledTimes(2)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('should unsubscribe on unmount', async () => {
|
|
59
|
+
const state = create(1)
|
|
60
|
+
const renders = jest.fn()
|
|
61
|
+
const { unmount } = renderHook(() => {
|
|
62
|
+
renders()
|
|
63
|
+
const value = useValue(state)
|
|
64
|
+
return value
|
|
65
|
+
})
|
|
66
|
+
act(() => {
|
|
67
|
+
state.set(2)
|
|
68
|
+
})
|
|
69
|
+
await waitFor(() => {})
|
|
70
|
+
expect(renders).toHaveBeenCalledTimes(2)
|
|
71
|
+
unmount()
|
|
72
|
+
act(() => {
|
|
73
|
+
state.set(3)
|
|
74
|
+
})
|
|
75
|
+
await waitFor(() => {})
|
|
76
|
+
expect(renders).toHaveBeenCalledTimes(2)
|
|
77
|
+
})
|
|
78
|
+
})
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { select } from './select'
|
|
2
|
+
import type { GetState, SetValue, State } from './types'
|
|
3
|
+
import { useValue } from './use-value'
|
|
4
|
+
import { createEmitter } from './utils/create-emitter'
|
|
5
|
+
import { isEqualBase } from './utils/is'
|
|
6
|
+
|
|
7
|
+
interface GetStateOptions<T> {
|
|
8
|
+
readonly get: () => T
|
|
9
|
+
readonly set?: (value: SetValue<T>) => void
|
|
10
|
+
readonly destroy: () => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let stateId = 0
|
|
14
|
+
function getStateId() {
|
|
15
|
+
return stateId++
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type FullState<T> = GetStateOptions<T>['set'] extends undefined ? GetState<T> : State<T>
|
|
19
|
+
/**
|
|
20
|
+
* This is just utility function to create state base data
|
|
21
|
+
*/
|
|
22
|
+
export function createState<T>(options: GetStateOptions<T>): FullState<T> {
|
|
23
|
+
const { get, destroy, set } = options
|
|
24
|
+
const isSet = !!set
|
|
25
|
+
|
|
26
|
+
const state: FullState<T> = function (selector) {
|
|
27
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
28
|
+
return useValue(state, selector)
|
|
29
|
+
}
|
|
30
|
+
state.isSet = isSet as true
|
|
31
|
+
state.id = getStateId()
|
|
32
|
+
state.emitter = createEmitter<T>(get)
|
|
33
|
+
state.destroy = destroy
|
|
34
|
+
state.listen = function (listener) {
|
|
35
|
+
return this.emitter.subscribe(() => {
|
|
36
|
+
listener(get())
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
state.withName = function (name) {
|
|
40
|
+
this.stateName = name
|
|
41
|
+
return this
|
|
42
|
+
}
|
|
43
|
+
state.select = function (selector, isSelectorEqual = isEqualBase) {
|
|
44
|
+
return select([state], selector, isSelectorEqual)
|
|
45
|
+
}
|
|
46
|
+
state.get = get
|
|
47
|
+
state.set = set as State<T>['set']
|
|
48
|
+
|
|
49
|
+
return state
|
|
50
|
+
}
|
package/src/create.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { canUpdate, handleAsyncUpdate } from './utils/common'
|
|
2
|
+
import { isEqualBase, isFunction, isSetValueFunction, isUndefined } from './utils/is'
|
|
3
|
+
import type { Cache, DefaultValue, IsEqual, SetValue, State } from './types'
|
|
4
|
+
import { createScheduler } from './scheduler'
|
|
5
|
+
import { subscribeToDevelopmentTools } from './debug/development-tools'
|
|
6
|
+
import { createState } from './create-state'
|
|
7
|
+
|
|
8
|
+
export const stateScheduler = createScheduler()
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Create state from a default value.
|
|
12
|
+
*/
|
|
13
|
+
export function create<T>(initialValue: DefaultValue<T>, isEqual: IsEqual<T> = isEqualBase): State<T> {
|
|
14
|
+
const cache: Cache<T> = {}
|
|
15
|
+
|
|
16
|
+
function getValue(): T {
|
|
17
|
+
try {
|
|
18
|
+
if (isUndefined(cache.current)) {
|
|
19
|
+
const value = isFunction(initialValue) ? initialValue() : initialValue
|
|
20
|
+
const resolvedValue = handleAsyncUpdate(cache, state.emitter.emit, value)
|
|
21
|
+
cache.current = resolvedValue
|
|
22
|
+
}
|
|
23
|
+
return cache.current
|
|
24
|
+
} catch (error) {
|
|
25
|
+
cache.current = error as T
|
|
26
|
+
}
|
|
27
|
+
return cache.current
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function setValue(value: SetValue<T>) {
|
|
31
|
+
if (cache.abortController) {
|
|
32
|
+
cache.abortController.abort()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const previous = getValue()
|
|
36
|
+
const newValue = isSetValueFunction(value) ? value(previous) : value
|
|
37
|
+
const resolvedValue = handleAsyncUpdate(cache, state.emitter.emit, newValue)
|
|
38
|
+
cache.current = resolvedValue
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const state = createState<T>({
|
|
42
|
+
get: getValue,
|
|
43
|
+
destroy() {
|
|
44
|
+
getValue()
|
|
45
|
+
clearScheduler()
|
|
46
|
+
state.emitter.clear()
|
|
47
|
+
cache.current = undefined
|
|
48
|
+
},
|
|
49
|
+
set(value: SetValue<T>) {
|
|
50
|
+
stateScheduler.schedule(state.id, value)
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const clearScheduler = stateScheduler.add(state.id, {
|
|
55
|
+
onFinish() {
|
|
56
|
+
cache.current = getValue()
|
|
57
|
+
if (!canUpdate(cache, isEqual)) {
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
state.emitter.emit()
|
|
61
|
+
},
|
|
62
|
+
onResolveItem: setValue,
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
subscribeToDevelopmentTools(state)
|
|
66
|
+
return state
|
|
67
|
+
}
|