muya 2.0.0-beta.3 → 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.
Files changed (64) hide show
  1. package/README.md +124 -195
  2. package/cjs/index.js +1 -1
  3. package/esm/create-state.js +1 -0
  4. package/esm/create.js +1 -1
  5. package/esm/debug/development-tools.js +1 -1
  6. package/esm/index.js +1 -1
  7. package/esm/scheduler.js +1 -0
  8. package/esm/select.js +1 -0
  9. package/esm/use-value.js +1 -0
  10. package/esm/utils/__tests__/is.test.js +1 -1
  11. package/esm/utils/common.js +1 -1
  12. package/esm/utils/is.js +1 -1
  13. package/package.json +12 -12
  14. package/src/__tests__/bench.test.tsx +3 -108
  15. package/src/__tests__/create.test.tsx +122 -70
  16. package/src/__tests__/scheduler.test.tsx +52 -0
  17. package/src/__tests__/select.test.tsx +127 -0
  18. package/src/__tests__/use-value.test.tsx +78 -0
  19. package/src/create-state.ts +50 -0
  20. package/src/create.ts +42 -73
  21. package/src/debug/development-tools.ts +18 -3
  22. package/src/index.ts +2 -1
  23. package/src/{utils/global-scheduler.ts → scheduler.ts} +9 -3
  24. package/src/select.ts +69 -0
  25. package/src/types.ts +57 -6
  26. package/src/use-value.ts +22 -0
  27. package/src/utils/__tests__/is.test.ts +24 -7
  28. package/src/utils/common.ts +35 -10
  29. package/src/utils/is.ts +5 -8
  30. package/types/create-state.d.ts +12 -0
  31. package/types/create.d.ts +6 -18
  32. package/types/debug/development-tools.d.ts +2 -9
  33. package/types/index.d.ts +2 -1
  34. package/types/{utils/scheduler.d.ts → scheduler.d.ts} +4 -1
  35. package/types/select.d.ts +10 -0
  36. package/types/types.d.ts +55 -5
  37. package/types/use-value.d.ts +2 -0
  38. package/types/utils/common.d.ts +6 -5
  39. package/types/utils/is.d.ts +3 -4
  40. package/esm/__tests__/create-async.test.js +0 -1
  41. package/esm/subscriber.js +0 -1
  42. package/esm/use.js +0 -1
  43. package/esm/utils/__tests__/context.test.js +0 -1
  44. package/esm/utils/__tests__/sub-memo.test.js +0 -1
  45. package/esm/utils/create-context.js +0 -1
  46. package/esm/utils/global-scheduler.js +0 -1
  47. package/esm/utils/scheduler.js +0 -1
  48. package/esm/utils/sub-memo.js +0 -1
  49. package/src/__tests__/create-async.test.ts +0 -88
  50. package/src/__tests__/subscriber.test.tsx +0 -89
  51. package/src/__tests__/use-async.test.tsx +0 -45
  52. package/src/__tests__/use.test.tsx +0 -125
  53. package/src/subscriber.ts +0 -165
  54. package/src/use.ts +0 -57
  55. package/src/utils/__tests__/context.test.ts +0 -198
  56. package/src/utils/__tests__/sub-memo.test.ts +0 -13
  57. package/src/utils/create-context.ts +0 -60
  58. package/src/utils/scheduler.ts +0 -59
  59. package/src/utils/sub-memo.ts +0 -49
  60. package/types/subscriber.d.ts +0 -25
  61. package/types/use.d.ts +0 -2
  62. package/types/utils/create-context.d.ts +0 -5
  63. package/types/utils/global-scheduler.d.ts +0 -5
  64. package/types/utils/sub-memo.d.ts +0 -7
@@ -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 { use } from '../use'
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 use(state)
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 use(state)
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
- })
@@ -1,107 +1,159 @@
1
1
  import { create } from '../create'
2
2
  import { waitFor } from '@testing-library/react'
3
- import { subscriber } from '../subscriber'
3
+ import { longPromise } from './test-utils'
4
4
 
5
5
  describe('create', () => {
6
6
  it('should get basic value states', async () => {
7
7
  const state1 = create(1)
8
8
  const state2 = create(2)
9
- expect(state1()).toBe(1)
10
- expect(state2()).toBe(2)
9
+ expect(state1.get()).toBe(1)
10
+ expect(state2.get()).toBe(2)
11
11
 
12
12
  state1.set(2)
13
13
  state2.set(3)
14
14
 
15
15
  await waitFor(() => {
16
- expect(state1()).toBe(2)
17
- expect(state2()).toBe(3)
16
+ expect(state1.get()).toBe(2)
17
+ expect(state2.get()).toBe(3)
18
18
  })
19
19
  })
20
- it('should get basic and derived value', async () => {
21
- const state1 = create(1)
22
- const state2 = create(2)
23
-
24
- function derived1() {
25
- return state1() + state2()
26
- }
27
-
28
- expect(state1()).toBe(1)
29
- expect(state2()).toBe(2)
30
- expect(derived1()).toBe(3)
31
-
32
- state1.set(2)
33
- state2.set(3)
34
-
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)
35
25
  await waitFor(() => {
36
- expect(state1()).toBe(2)
37
- expect(state2()).toBe(3)
38
- expect(derived1()).toBe(5)
26
+ expect(listener).toHaveBeenCalledWith(2)
39
27
  })
40
28
  })
41
- it('should subscribe to context and notified it', async () => {
42
- const state1 = create(1)
43
- const state2 = create(2)
44
-
45
- function derivedNested() {
46
- return state1() + state2()
47
- }
48
- function derived() {
49
- return state1() + state2() + derivedNested()
50
- }
51
-
52
- let updatesCounter = 0
53
29
 
54
- const sub = subscriber(derived)
55
-
56
- sub.listen(() => {
57
- updatesCounter++
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()
58
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
+ })
59
58
 
60
- // check if there is not maximum call stack
61
- sub()
62
- sub()
63
- sub()
64
-
65
- // check if not assigned multiple times, but only once
66
- expect(state1.emitter.getSize()).toBe(1)
67
- expect(state2.emitter.getSize()).toBe(1)
68
- expect(sub.emitter.getSize()).toBe(1)
69
- expect(sub.emitter.getSnapshot()).toBe(6)
70
- state1.set(2)
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
+ })
71
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)
72
73
  await waitFor(() => {
73
- expect(sub()).toBe(8)
74
- expect(updatesCounter).toBe(1)
74
+ expect(state.get()).toBe(1)
75
+ expect(listener).toHaveBeenCalledWith(1)
75
76
  })
77
+ })
76
78
 
77
- state2.set(3)
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
+ })
78
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)
79
97
  await waitFor(() => {
80
- expect(sub()).toBe(10)
81
- expect(updatesCounter).toBe(2)
98
+ expect(listener).not.toHaveBeenCalled()
82
99
  })
100
+ })
83
101
 
84
- expect(state1.emitter.getSize()).toBe(1)
85
- expect(state2.emitter.getSize()).toBe(1)
86
- expect(sub.emitter.getSize()).toBe(1)
87
- expect(sub.emitter.getSnapshot()).toBe(10)
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
+ })
88
112
 
89
- sub.destroy()
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)
90
117
 
91
- expect(state1.emitter.getSize()).toBe(0)
92
- expect(state2.emitter.getSize()).toBe(0)
93
- expect(sub.emitter.getSize()).toBe(0)
118
+ state.set({ count: 2 })
119
+ await waitFor(() => {
120
+ expect(select.get()).toBe(2)
121
+ })
94
122
  })
95
- it('should subscribe and set snapshot', async () => {
96
- const state = create(1)
97
- const sub = subscriber(state)
98
- sub()
99
-
100
- expect(sub.emitter.getSnapshot()).toBe(1)
101
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))
102
136
  state.set(2)
103
137
  await waitFor(() => {
104
- expect(sub.emitter.getSnapshot()).toBe(2)
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)
105
157
  })
106
158
  })
107
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
+ }