muya 1.1.0 → 2.0.0-beta.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/README.md +201 -138
- package/cjs/index.js +1 -1
- package/esm/__tests__/create-async.test.js +1 -0
- package/esm/__tests__/test-utils.js +1 -0
- package/esm/create.js +1 -1
- package/esm/index.js +1 -1
- package/esm/subscriber.js +1 -0
- package/esm/types.js +1 -1
- package/esm/use.js +1 -0
- package/esm/utils/__tests__/context.test.js +1 -0
- package/esm/utils/__tests__/is.test.js +1 -0
- package/esm/utils/__tests__/shallow.test.js +1 -0
- package/esm/utils/__tests__/sub-memo.test.js +1 -0
- package/esm/utils/common.js +1 -0
- package/esm/utils/create-context.js +1 -0
- package/esm/utils/create-emitter.js +1 -0
- package/esm/utils/is.js +1 -0
- package/esm/utils/scheduler.js +1 -0
- package/esm/utils/shallow.js +1 -0
- package/esm/utils/sub-memo.js +1 -0
- package/package.json +1 -1
- package/packages/core/__tests__/bench.test.tsx +261 -0
- package/packages/core/__tests__/create-async.test.ts +88 -0
- package/packages/core/__tests__/create.test.tsx +107 -0
- package/packages/core/__tests__/test-utils.ts +40 -0
- package/packages/core/__tests__/use-async.test.tsx +44 -0
- package/packages/core/__tests__/use.test.tsx +76 -0
- package/packages/core/create.ts +67 -0
- package/packages/core/index.ts +4 -0
- package/packages/core/subscriber.ts +121 -0
- package/packages/core/types.ts +15 -0
- package/packages/core/use.ts +59 -0
- package/packages/core/utils/__tests__/context.test.ts +198 -0
- package/packages/core/utils/__tests__/is.test.ts +74 -0
- package/packages/core/utils/__tests__/shallow.test.ts +418 -0
- package/packages/core/utils/__tests__/sub-memo.test.ts +13 -0
- package/packages/core/utils/common.ts +48 -0
- package/packages/core/utils/create-context.ts +60 -0
- package/packages/core/utils/create-emitter.ts +53 -0
- package/packages/core/utils/is.ts +43 -0
- package/packages/core/utils/scheduler.ts +59 -0
- package/{src → packages/core/utils}/shallow.ts +3 -6
- package/packages/core/utils/sub-memo.ts +37 -0
- package/types/__tests__/test-utils.d.ts +20 -0
- package/types/create.d.ts +14 -21
- package/types/index.d.ts +2 -4
- package/types/subscriber.d.ts +25 -0
- package/types/types.d.ts +9 -65
- package/types/use.d.ts +2 -0
- package/types/utils/common.d.ts +15 -0
- package/types/utils/create-context.d.ts +5 -0
- package/types/utils/create-emitter.d.ts +20 -0
- package/types/utils/is.d.ts +11 -0
- package/types/utils/scheduler.d.ts +6 -0
- package/types/utils/sub-memo.d.ts +6 -0
- package/esm/common.js +0 -1
- package/esm/create-base-state.js +0 -1
- package/esm/create-emitter.js +0 -1
- package/esm/create-getter-state.js +0 -1
- package/esm/is.js +0 -1
- package/esm/merge.js +0 -1
- package/esm/select.js +0 -1
- package/esm/shallow.js +0 -1
- package/esm/use-state-value.js +0 -1
- package/src/common.ts +0 -28
- package/src/create-base-state.ts +0 -35
- package/src/create-emitter.ts +0 -24
- package/src/create-getter-state.ts +0 -19
- package/src/create.ts +0 -102
- package/src/index.ts +0 -6
- package/src/is.ts +0 -36
- package/src/merge.ts +0 -41
- package/src/select.ts +0 -33
- package/src/state.test.tsx +0 -647
- package/src/types.ts +0 -94
- package/src/use-state-value.ts +0 -29
- package/types/common.d.ts +0 -7
- package/types/create-base-state.d.ts +0 -10
- package/types/create-emitter.d.ts +0 -7
- package/types/create-getter-state.d.ts +0 -6
- package/types/is.d.ts +0 -10
- package/types/merge.d.ts +0 -2
- package/types/select.d.ts +0 -2
- package/types/use-state-value.d.ts +0 -10
- /package/types/{shallow.d.ts → utils/shallow.d.ts} +0 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/* eslint-disable unicorn/consistent-function-scoping */
|
|
2
|
+
/* eslint-disable no-console */
|
|
3
|
+
import { act, renderHook } from '@testing-library/react-hooks'
|
|
4
|
+
import { useStore, create as zustand } from 'zustand'
|
|
5
|
+
import { useEffect, useState } from 'react'
|
|
6
|
+
import { use } from '../use'
|
|
7
|
+
import { atom, useAtom } from 'jotai'
|
|
8
|
+
import { create } from '../create'
|
|
9
|
+
|
|
10
|
+
function renderPerfHook<T>(hook: () => T, getValue: (data: T) => number, toBe: number) {
|
|
11
|
+
let onResolve = (_value: number) => {}
|
|
12
|
+
const resolvePromise = new Promise<number>((resolve) => {
|
|
13
|
+
onResolve = resolve
|
|
14
|
+
})
|
|
15
|
+
const start = performance.now()
|
|
16
|
+
const { result, waitFor } = renderHook(() => {
|
|
17
|
+
const data = hook()
|
|
18
|
+
const count = getValue(data)
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (count === toBe) {
|
|
21
|
+
const end = performance.now()
|
|
22
|
+
onResolve(end - start)
|
|
23
|
+
}
|
|
24
|
+
}, [count])
|
|
25
|
+
return data
|
|
26
|
+
})
|
|
27
|
+
return { result, waitFor, resolvePromise }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('benchmarks comparison measure', () => {
|
|
31
|
+
const reRendersBefore = jest.fn()
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
jest.clearAllMocks()
|
|
35
|
+
})
|
|
36
|
+
const counts = [1000, 10_000]
|
|
37
|
+
for (const count of counts) {
|
|
38
|
+
describe(`Count ${count}`, () => {
|
|
39
|
+
it(`should benchmark ${count} muya first run - idk slow`, async () => {
|
|
40
|
+
const state = create(1)
|
|
41
|
+
// let count = 0
|
|
42
|
+
|
|
43
|
+
const { result, resolvePromise } = renderPerfHook(
|
|
44
|
+
() => {
|
|
45
|
+
reRendersBefore()
|
|
46
|
+
return use(state)
|
|
47
|
+
},
|
|
48
|
+
(data) => data,
|
|
49
|
+
count - 1,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
for (let index = 0; index < count; index++) {
|
|
53
|
+
act(() => {
|
|
54
|
+
state.set(index)
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const time = await resolvePromise
|
|
59
|
+
expect(result.current).toBe(count - 1)
|
|
60
|
+
console.log('Time', time)
|
|
61
|
+
console.log('Renders', reRendersBefore.mock.calls.length)
|
|
62
|
+
})
|
|
63
|
+
it(`should benchmark jotai ${count}`, async () => {
|
|
64
|
+
const state = atom(1)
|
|
65
|
+
|
|
66
|
+
const { result, resolvePromise } = renderPerfHook(
|
|
67
|
+
() => {
|
|
68
|
+
reRendersBefore()
|
|
69
|
+
return useAtom(state)
|
|
70
|
+
},
|
|
71
|
+
(data) => data[0],
|
|
72
|
+
count - 1,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
for (let index = 0; index < count; index++) {
|
|
76
|
+
act(() => {
|
|
77
|
+
result.current[1](index)
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const time = await resolvePromise
|
|
82
|
+
expect(result.current[0]).toBe(count - 1)
|
|
83
|
+
console.log('Time', time)
|
|
84
|
+
console.log('Renders', reRendersBefore.mock.calls.length)
|
|
85
|
+
})
|
|
86
|
+
it(`should benchmark zustand ${count}`, async () => {
|
|
87
|
+
const state = zustand((_set) => ({ state: 1 }))
|
|
88
|
+
const { result, resolvePromise } = renderPerfHook(
|
|
89
|
+
() => {
|
|
90
|
+
reRendersBefore()
|
|
91
|
+
return useStore(state)
|
|
92
|
+
},
|
|
93
|
+
(data) => data as number,
|
|
94
|
+
count - 1,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
for (let index = 0; index < count; index++) {
|
|
98
|
+
act(() => {
|
|
99
|
+
state.setState(index)
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const time = await resolvePromise
|
|
104
|
+
expect(result.current).toBe(count - 1)
|
|
105
|
+
console.log('Time', time)
|
|
106
|
+
console.log('Renders', reRendersBefore.mock.calls.length)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it(`should benchmark react ${count}`, async () => {
|
|
110
|
+
const { result, resolvePromise } = renderPerfHook(
|
|
111
|
+
() => {
|
|
112
|
+
reRendersBefore()
|
|
113
|
+
return useState(1)
|
|
114
|
+
},
|
|
115
|
+
(data) => data[0],
|
|
116
|
+
count - 1,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
for (let index = 0; index < count; index++) {
|
|
120
|
+
act(() => {
|
|
121
|
+
result.current[1](index)
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const time = await resolvePromise
|
|
126
|
+
expect(result.current[0]).toBe(count - 1)
|
|
127
|
+
console.log('Time', time)
|
|
128
|
+
console.log('Renders', reRendersBefore.mock.calls.length)
|
|
129
|
+
})
|
|
130
|
+
it(`should benchmark ${count} muya`, async () => {
|
|
131
|
+
const state = create(1)
|
|
132
|
+
// let count = 0
|
|
133
|
+
|
|
134
|
+
const { result, resolvePromise } = renderPerfHook(
|
|
135
|
+
() => {
|
|
136
|
+
reRendersBefore()
|
|
137
|
+
return use(state)
|
|
138
|
+
},
|
|
139
|
+
(data) => data,
|
|
140
|
+
count - 1,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
for (let index = 0; index < count; index++) {
|
|
144
|
+
act(() => {
|
|
145
|
+
state.set(index)
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const time = await resolvePromise
|
|
150
|
+
expect(result.current).toBe(count - 1)
|
|
151
|
+
console.log('Time', time)
|
|
152
|
+
console.log('Renders', reRendersBefore.mock.calls.length)
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
describe('benchmarks comparison between others', () => {
|
|
159
|
+
const reRendersBefore = jest.fn()
|
|
160
|
+
|
|
161
|
+
beforeEach(() => {
|
|
162
|
+
jest.clearAllMocks()
|
|
163
|
+
})
|
|
164
|
+
// const count = 10000
|
|
165
|
+
|
|
166
|
+
const counts = [100]
|
|
167
|
+
for (const count of counts) {
|
|
168
|
+
describe(`Count ${count}`, () => {
|
|
169
|
+
it(`should benchmark jotai ${count}`, async () => {
|
|
170
|
+
const state = atom(1)
|
|
171
|
+
const start = performance.now()
|
|
172
|
+
const { result, waitFor } = renderHook(() => {
|
|
173
|
+
reRendersBefore()
|
|
174
|
+
return useAtom(state)
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
for (let index = 0; index < count; index++) {
|
|
178
|
+
act(() => {
|
|
179
|
+
result.current[1](index)
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
await waitFor(() => {
|
|
184
|
+
expect(result.current[0]).toBe(count - 1)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
const end = performance.now()
|
|
188
|
+
console.log('Time', end - start)
|
|
189
|
+
console.log('Renders', reRendersBefore.mock.calls.length)
|
|
190
|
+
// expect(reRendersBefore).toHaveBeenCalledTimes(3)
|
|
191
|
+
})
|
|
192
|
+
it(`should benchmark zustand ${count}`, async () => {
|
|
193
|
+
const state = zustand((_set) => ({ state: 1 }))
|
|
194
|
+
const start = performance.now()
|
|
195
|
+
const { result, waitFor } = renderHook(() => {
|
|
196
|
+
reRendersBefore()
|
|
197
|
+
return useStore(state)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
for (let index = 0; index < count; index++) {
|
|
201
|
+
act(() => {
|
|
202
|
+
state.setState(index)
|
|
203
|
+
})
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
await waitFor(() => {
|
|
207
|
+
expect(result.current).toBe(count - 1)
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
const end = performance.now()
|
|
211
|
+
console.log('Time', end - start)
|
|
212
|
+
console.log('Renders', reRendersBefore.mock.calls.length)
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it(`should benchmark react ${count}`, async () => {
|
|
216
|
+
const start = performance.now()
|
|
217
|
+
const { result, waitFor } = renderHook(() => {
|
|
218
|
+
reRendersBefore()
|
|
219
|
+
return useState(1)
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
for (let index = 0; index < count; index++) {
|
|
223
|
+
act(() => {
|
|
224
|
+
result.current[1](index)
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
await waitFor(() => {
|
|
229
|
+
expect(result.current[0]).toBe(count - 1)
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
const end = performance.now()
|
|
233
|
+
console.log('Time', end - start)
|
|
234
|
+
console.log('Renders', reRendersBefore.mock.calls.length)
|
|
235
|
+
})
|
|
236
|
+
})
|
|
237
|
+
it(`should benchmark ${count} muya`, async () => {
|
|
238
|
+
const state = create(1)
|
|
239
|
+
const start = performance.now()
|
|
240
|
+
const { result, waitFor } = renderHook(() => {
|
|
241
|
+
reRendersBefore()
|
|
242
|
+
return use(state)
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
for (let index = 0; index < count; index++) {
|
|
246
|
+
act(() => {
|
|
247
|
+
state.set(index)
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
await waitFor(() => {
|
|
252
|
+
expect(result.current).toBe(count - 1)
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
const end = performance.now()
|
|
256
|
+
console.log('Time', end - start)
|
|
257
|
+
console.log('Renders', reRendersBefore.mock.calls.length)
|
|
258
|
+
// expect(reRendersBefore).toHaveBeenCalledTimes(2)
|
|
259
|
+
})
|
|
260
|
+
}
|
|
261
|
+
})
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { create } from '../create'
|
|
2
|
+
import { waitFor } from '@testing-library/react'
|
|
3
|
+
import { longPromise } from './test-utils'
|
|
4
|
+
import { isPromise } from '../utils/is'
|
|
5
|
+
import { subscriber } from '../subscriber'
|
|
6
|
+
|
|
7
|
+
describe('create', () => {
|
|
8
|
+
it('should subscribe to context and notified it with parameters', async () => {
|
|
9
|
+
const state1 = create(1)
|
|
10
|
+
const state2 = create(2)
|
|
11
|
+
|
|
12
|
+
function derivedNested() {
|
|
13
|
+
return state1() + state2()
|
|
14
|
+
}
|
|
15
|
+
async function derived(plus: number) {
|
|
16
|
+
return state1() + state2() + derivedNested() + plus
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let updatesCounter = 0
|
|
20
|
+
const sub = subscriber(() => derived(10))
|
|
21
|
+
expect(isPromise(sub.emitter.getSnapshot())).toBe(true)
|
|
22
|
+
sub.listen(async () => {
|
|
23
|
+
updatesCounter++
|
|
24
|
+
})
|
|
25
|
+
expect(updatesCounter).toBe(0)
|
|
26
|
+
// check if there is not maximum call stack
|
|
27
|
+
expect(await sub()).toBe(16)
|
|
28
|
+
|
|
29
|
+
state1.set(2)
|
|
30
|
+
//
|
|
31
|
+
await waitFor(async () => {})
|
|
32
|
+
expect(await sub()).toBe(18)
|
|
33
|
+
|
|
34
|
+
expect(updatesCounter).toBe(4)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('should async subscribe to context and notified it', async () => {
|
|
38
|
+
const state1 = create(1)
|
|
39
|
+
const state2 = create(Promise.resolve(2))
|
|
40
|
+
|
|
41
|
+
async function derivedNested() {
|
|
42
|
+
await longPromise()
|
|
43
|
+
return state1() + (await state2())
|
|
44
|
+
}
|
|
45
|
+
async function derived() {
|
|
46
|
+
return state1() + (await state2()) + (await derivedNested())
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let updatesCounter = 0
|
|
50
|
+
|
|
51
|
+
const sub = subscriber(derived)
|
|
52
|
+
|
|
53
|
+
sub.listen(() => {
|
|
54
|
+
updatesCounter++
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
// check if there is not maximum call stack
|
|
58
|
+
sub()
|
|
59
|
+
|
|
60
|
+
// check if not assigned multiple times, but only once
|
|
61
|
+
expect(state1.emitter.getSize()).toBe(1)
|
|
62
|
+
expect(state2.emitter.getSize()).toBe(1)
|
|
63
|
+
expect(sub.emitter.getSize()).toBe(1)
|
|
64
|
+
state1.set(2)
|
|
65
|
+
|
|
66
|
+
await waitFor(async () => {
|
|
67
|
+
expect(await sub()).toBe(8)
|
|
68
|
+
expect(updatesCounter).toBe(5)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
state2.set(3)
|
|
72
|
+
|
|
73
|
+
await waitFor(async () => {
|
|
74
|
+
expect(await sub()).toBe(10)
|
|
75
|
+
expect(updatesCounter).toBe(10)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
expect(state1.emitter.getSize()).toBe(1)
|
|
79
|
+
expect(state2.emitter.getSize()).toBe(1)
|
|
80
|
+
expect(sub.emitter.getSize()).toBe(1)
|
|
81
|
+
|
|
82
|
+
sub.destroy()
|
|
83
|
+
|
|
84
|
+
expect(state1.emitter.getSize()).toBe(0)
|
|
85
|
+
expect(state2.emitter.getSize()).toBe(0)
|
|
86
|
+
expect(sub.emitter.getSize()).toBe(0)
|
|
87
|
+
})
|
|
88
|
+
})
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { create } from '../create'
|
|
2
|
+
import { waitFor } from '@testing-library/react'
|
|
3
|
+
import { subscriber } from '../subscriber'
|
|
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()).toBe(1)
|
|
10
|
+
expect(state2()).toBe(2)
|
|
11
|
+
|
|
12
|
+
state1.set(2)
|
|
13
|
+
state2.set(3)
|
|
14
|
+
|
|
15
|
+
await waitFor(() => {
|
|
16
|
+
expect(state1()).toBe(2)
|
|
17
|
+
expect(state2()).toBe(3)
|
|
18
|
+
})
|
|
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
|
+
|
|
35
|
+
await waitFor(() => {
|
|
36
|
+
expect(state1()).toBe(2)
|
|
37
|
+
expect(state2()).toBe(3)
|
|
38
|
+
expect(derived1()).toBe(5)
|
|
39
|
+
})
|
|
40
|
+
})
|
|
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
|
+
|
|
54
|
+
const sub = subscriber(derived)
|
|
55
|
+
|
|
56
|
+
sub.listen(() => {
|
|
57
|
+
updatesCounter++
|
|
58
|
+
})
|
|
59
|
+
|
|
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)
|
|
71
|
+
|
|
72
|
+
await waitFor(() => {
|
|
73
|
+
expect(sub()).toBe(8)
|
|
74
|
+
expect(updatesCounter).toBe(1)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
state2.set(3)
|
|
78
|
+
|
|
79
|
+
await waitFor(() => {
|
|
80
|
+
expect(sub()).toBe(10)
|
|
81
|
+
expect(updatesCounter).toBe(2)
|
|
82
|
+
})
|
|
83
|
+
|
|
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)
|
|
88
|
+
|
|
89
|
+
sub.destroy()
|
|
90
|
+
|
|
91
|
+
expect(state1.emitter.getSize()).toBe(0)
|
|
92
|
+
expect(state2.emitter.getSize()).toBe(0)
|
|
93
|
+
expect(sub.emitter.getSize()).toBe(0)
|
|
94
|
+
})
|
|
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
|
+
|
|
102
|
+
state.set(2)
|
|
103
|
+
await waitFor(() => {
|
|
104
|
+
expect(sub.emitter.getSnapshot()).toBe(2)
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
})
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Component } from 'react'
|
|
2
|
+
|
|
3
|
+
export function longPromise(time = 200): Promise<number> {
|
|
4
|
+
return new Promise((resolve) => {
|
|
5
|
+
setTimeout(() => {
|
|
6
|
+
resolve(0)
|
|
7
|
+
}, time)
|
|
8
|
+
})
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// ErrorBoundary Component
|
|
12
|
+
export class ErrorBoundary extends Component<
|
|
13
|
+
{ fallback: React.ReactNode; children: React.ReactNode },
|
|
14
|
+
{ hasError: boolean; error: Error | null }
|
|
15
|
+
> {
|
|
16
|
+
constructor(props: { fallback: React.ReactNode; children: React.ReactNode }) {
|
|
17
|
+
super(props)
|
|
18
|
+
this.state = { hasError: false, error: null }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static getDerivedStateFromError(error: Error) {
|
|
22
|
+
// Update state so the next render shows the fallback UI.
|
|
23
|
+
return { hasError: true, error }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
|
27
|
+
// You can log the error to an error reporting service here
|
|
28
|
+
// eslint-disable-next-line no-console
|
|
29
|
+
console.error('ErrorBoundary caught an error:', error, errorInfo)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
render() {
|
|
33
|
+
if (this.state.hasError) {
|
|
34
|
+
// Render fallback UI
|
|
35
|
+
return this.props.fallback
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return this.props.children
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
|
2
|
+
import { renderHook } from '@testing-library/react-hooks'
|
|
3
|
+
import { create } from '../create'
|
|
4
|
+
import { use } from '../use'
|
|
5
|
+
import { Suspense } from 'react'
|
|
6
|
+
import { longPromise } from './test-utils'
|
|
7
|
+
|
|
8
|
+
describe('use-create', () => {
|
|
9
|
+
const reRendersBefore = jest.fn()
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
jest.clearAllMocks()
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('should test sub hook', async () => {
|
|
16
|
+
const userState = create({ name: 'John', age: 30 })
|
|
17
|
+
async function getDataWithUser() {
|
|
18
|
+
const result = await fetch('https://jsonplaceholder.typicode.com/todos/1')
|
|
19
|
+
const json = await result.json()
|
|
20
|
+
return { age: userState().age, ...json }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const suspenseFunction = jest.fn()
|
|
24
|
+
function Loading() {
|
|
25
|
+
suspenseFunction()
|
|
26
|
+
return <div>Loading...</div>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const { result, waitFor } = renderHook(
|
|
30
|
+
() => {
|
|
31
|
+
reRendersBefore()
|
|
32
|
+
const data = use(getDataWithUser)
|
|
33
|
+
return data
|
|
34
|
+
},
|
|
35
|
+
// @ts-expect-error
|
|
36
|
+
{ wrapper: ({ children }) => <Suspense fallback={<Loading />}>{children}</Suspense> },
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
await longPromise(1000)
|
|
40
|
+
await waitFor(() => {})
|
|
41
|
+
expect(suspenseFunction).toHaveBeenCalledTimes(1)
|
|
42
|
+
expect(result.current).toEqual({ userId: 1, id: 1, title: 'delectus aut autem', completed: false, age: 30 })
|
|
43
|
+
})
|
|
44
|
+
})
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-shadow */
|
|
2
|
+
/* eslint-disable no-shadow */
|
|
3
|
+
import { act, renderHook } from '@testing-library/react-hooks'
|
|
4
|
+
import { create } from '../create'
|
|
5
|
+
import { use } from '../use'
|
|
6
|
+
import { subscriber } from '../subscriber'
|
|
7
|
+
import { waitFor } from '@testing-library/react'
|
|
8
|
+
|
|
9
|
+
describe('use-create', () => {
|
|
10
|
+
const reRendersBefore = jest.fn()
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
jest.clearAllMocks()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('should test sub hook', async () => {
|
|
17
|
+
const state = create(1)
|
|
18
|
+
|
|
19
|
+
const sub = subscriber(state)
|
|
20
|
+
expect(sub()).toBe(1)
|
|
21
|
+
|
|
22
|
+
act(() => {
|
|
23
|
+
state.set(2)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
await waitFor(() => {})
|
|
27
|
+
expect(sub()).toBe(2)
|
|
28
|
+
})
|
|
29
|
+
it('should test use hook', async () => {
|
|
30
|
+
const state = create(1)
|
|
31
|
+
|
|
32
|
+
const { result, waitFor } = renderHook(() => {
|
|
33
|
+
reRendersBefore()
|
|
34
|
+
return use(state)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
state.set(2)
|
|
38
|
+
|
|
39
|
+
await waitFor(() => {})
|
|
40
|
+
expect(result.current).toBe(2)
|
|
41
|
+
expect(reRendersBefore).toHaveBeenCalledTimes(2)
|
|
42
|
+
|
|
43
|
+
state.set(3)
|
|
44
|
+
|
|
45
|
+
await waitFor(() => {})
|
|
46
|
+
expect(result.current).toBe(3)
|
|
47
|
+
expect(reRendersBefore).toHaveBeenCalledTimes(3)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('should test derived state', async () => {
|
|
51
|
+
const state1 = create(1)
|
|
52
|
+
const state2 = create(2)
|
|
53
|
+
|
|
54
|
+
function derivedBefore(plusValue: number) {
|
|
55
|
+
return state1() + state2() + plusValue
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function derived() {
|
|
59
|
+
return state1() + state2() + derivedBefore(10)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const { result, waitFor } = renderHook(() => {
|
|
63
|
+
reRendersBefore()
|
|
64
|
+
return use(derived)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
act(() => {
|
|
68
|
+
state1.set(2)
|
|
69
|
+
state2.set(3)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
await waitFor(() => {})
|
|
73
|
+
expect(result.current).toBe(20)
|
|
74
|
+
expect(reRendersBefore).toHaveBeenCalledTimes(2)
|
|
75
|
+
})
|
|
76
|
+
})
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { canUpdate, generateId } from './utils/common'
|
|
2
|
+
import type { Emitter } from './utils/create-emitter'
|
|
3
|
+
import { createEmitter } from './utils/create-emitter'
|
|
4
|
+
import { isEqualBase, isFunction, isSetValueFunction, isUndefined } from './utils/is'
|
|
5
|
+
import { createScheduler } from './utils/scheduler'
|
|
6
|
+
import type { Cache, Callable, DefaultValue, IsEqual, Listener, SetValue } from './types'
|
|
7
|
+
import { context } from './subscriber'
|
|
8
|
+
|
|
9
|
+
interface RawState<T> {
|
|
10
|
+
(): T
|
|
11
|
+
id: number
|
|
12
|
+
set: (value: SetValue<T>) => void
|
|
13
|
+
emitter: Emitter<T>
|
|
14
|
+
listen: Listener<T>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type State<T> = {
|
|
18
|
+
readonly [K in keyof RawState<T>]: RawState<T>[K]
|
|
19
|
+
} & Callable<T>
|
|
20
|
+
|
|
21
|
+
export function create<T>(initialValue: DefaultValue<T>, isEqual: IsEqual<T> = isEqualBase): State<T> {
|
|
22
|
+
const cache: Cache<T> = {}
|
|
23
|
+
|
|
24
|
+
function getValue(): T {
|
|
25
|
+
if (isUndefined(cache.current)) {
|
|
26
|
+
cache.current = isFunction(initialValue) ? initialValue() : initialValue
|
|
27
|
+
}
|
|
28
|
+
return cache.current
|
|
29
|
+
}
|
|
30
|
+
function resolveValue(value: SetValue<T>) {
|
|
31
|
+
const previous = getValue()
|
|
32
|
+
cache.current = isSetValueFunction(value) ? value(previous) : value
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const schedule = createScheduler<SetValue<T>>({
|
|
36
|
+
onFinish() {
|
|
37
|
+
cache.current = getValue()
|
|
38
|
+
if (!canUpdate(cache, isEqual)) {
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
state.emitter.emit()
|
|
42
|
+
},
|
|
43
|
+
onResolveItem: resolveValue,
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
const state: RawState<T> = function () {
|
|
47
|
+
const stateValue = getValue()
|
|
48
|
+
const ctx = context.use()
|
|
49
|
+
if (ctx && !state.emitter.contains(ctx.sub)) {
|
|
50
|
+
ctx.addEmitter(state.emitter)
|
|
51
|
+
}
|
|
52
|
+
return stateValue
|
|
53
|
+
}
|
|
54
|
+
state.listen = function (listener: (value: T) => void) {
|
|
55
|
+
return state.emitter.subscribe(() => {
|
|
56
|
+
const final = cache.current
|
|
57
|
+
if (isUndefined(final)) {
|
|
58
|
+
throw new Error('The value is undefined')
|
|
59
|
+
}
|
|
60
|
+
listener(final)
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
state.emitter = createEmitter<T>(() => state())
|
|
64
|
+
state.id = generateId()
|
|
65
|
+
state.set = schedule
|
|
66
|
+
return state
|
|
67
|
+
}
|