Skip to content

Commit f5d4b74

Browse files
author
David Maskasky
committed
fix: full rewrite of ScopeProvider to address known issues
- fixes: #25, #36
1 parent 3198ee1 commit f5d4b74

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+10923
-9
lines changed

.eslintrc.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
"@typescript-eslint/explicit-function-return-type": "off",
2626
"@typescript-eslint/explicit-module-boundary-types": "off",
2727
"@typescript-eslint/no-explicit-any": "off",
28-
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
28+
"@typescript-eslint/no-non-null-asserted-optional-chain": "off",
29+
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
2930
"react/button-has-type": "off",
3031
"react/jsx-filename-extension": ["error", { "extensions": [".js", ".tsx"] }],
3132
"react/prop-types": "off",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
import { StrictMode, Suspense, useState } from 'react'
2+
import { render, waitFor } from '@testing-library/react'
3+
import userEvent from '@testing-library/user-event'
4+
import { useAtomValue, useSetAtom } from '../../../../jotai/react'
5+
import { atom } from '../../../../jotai/vanilla'
6+
7+
describe('abortable atom test', () => {
8+
it('can abort with signal.aborted', async () => {
9+
const countAtom = atom(0)
10+
let abortedCount = 0
11+
const resolve: (() => void)[] = []
12+
const derivedAtom = atom(async (get, { signal }) => {
13+
const count = get(countAtom)
14+
await new Promise<void>((r) => {
15+
resolve.push(r)
16+
})
17+
if (signal.aborted) {
18+
++abortedCount
19+
}
20+
return count
21+
})
22+
23+
function Component() {
24+
const count = useAtomValue(derivedAtom)
25+
return <div>count: {count}</div>
26+
}
27+
28+
function Controls() {
29+
const setCount = useSetAtom(countAtom)
30+
return <button onClick={() => setCount((c) => c + 1)}>button</button>
31+
}
32+
33+
const { findByText, getByText } = render(
34+
<StrictMode>
35+
<Suspense fallback="loading">
36+
<Component />
37+
<Controls />
38+
</Suspense>
39+
</StrictMode>,
40+
)
41+
42+
await findByText('loading')
43+
44+
resolve.splice(0).forEach((fn) => fn())
45+
await findByText('count: 0')
46+
expect(abortedCount).toBe(0)
47+
48+
await userEvent.click(getByText('button'))
49+
await userEvent.click(getByText('button'))
50+
resolve.splice(0).forEach((fn) => fn())
51+
await findByText('count: 2')
52+
53+
expect(abortedCount).toBe(1)
54+
55+
await userEvent.click(getByText('button'))
56+
resolve.splice(0).forEach((fn) => fn())
57+
await findByText('count: 3')
58+
expect(abortedCount).toBe(1)
59+
})
60+
61+
it('can abort with event listener', async () => {
62+
const countAtom = atom(0)
63+
let abortedCount = 0
64+
const resolve: (() => void)[] = []
65+
const derivedAtom = atom(async (get, { signal }) => {
66+
const count = get(countAtom)
67+
const callback = () => {
68+
++abortedCount
69+
}
70+
signal.addEventListener('abort', callback)
71+
await new Promise<void>((r) => resolve.push(r))
72+
signal.removeEventListener('abort', callback)
73+
return count
74+
})
75+
76+
function Component() {
77+
const count = useAtomValue(derivedAtom)
78+
return <div>count: {count}</div>
79+
}
80+
81+
function Controls() {
82+
const setCount = useSetAtom(countAtom)
83+
return <button onClick={() => setCount((c) => c + 1)}>button</button>
84+
}
85+
86+
const { findByText, getByText } = render(
87+
<StrictMode>
88+
<Suspense fallback="loading">
89+
<Component />
90+
<Controls />
91+
</Suspense>
92+
</StrictMode>,
93+
)
94+
95+
await findByText('loading')
96+
resolve.splice(0).forEach((fn) => fn())
97+
await findByText('count: 0')
98+
99+
expect(abortedCount).toBe(0)
100+
101+
await userEvent.click(getByText('button'))
102+
await userEvent.click(getByText('button'))
103+
resolve.splice(0).forEach((fn) => fn())
104+
await findByText('count: 2')
105+
106+
expect(abortedCount).toBe(1)
107+
108+
await userEvent.click(getByText('button'))
109+
resolve.splice(0).forEach((fn) => fn())
110+
await findByText('count: 3')
111+
112+
expect(abortedCount).toBe(1)
113+
})
114+
115+
it('does not abort on unmount', async () => {
116+
const countAtom = atom(0)
117+
let abortedCount = 0
118+
const resolve: (() => void)[] = []
119+
const derivedAtom = atom(async (get, { signal }) => {
120+
const count = get(countAtom)
121+
await new Promise<void>((r) => resolve.push(r))
122+
if (signal.aborted) {
123+
++abortedCount
124+
}
125+
return count
126+
})
127+
128+
function Component() {
129+
const count = useAtomValue(derivedAtom)
130+
return <div>count: {count}</div>
131+
}
132+
133+
function Parent() {
134+
const setCount = useSetAtom(countAtom)
135+
const [show, setShow] = useState(true)
136+
return (
137+
<>
138+
{show ? <Component /> : 'hidden'}
139+
<button onClick={() => setCount((c) => c + 1)}>button</button>
140+
<button onClick={() => setShow((x) => !x)}>toggle</button>
141+
</>
142+
)
143+
}
144+
145+
const { findByText, getByText } = render(
146+
<StrictMode>
147+
<Suspense fallback="loading">
148+
<Parent />
149+
</Suspense>
150+
</StrictMode>,
151+
)
152+
153+
await findByText('loading')
154+
155+
resolve.splice(0).forEach((fn) => fn())
156+
await findByText('count: 0')
157+
expect(abortedCount).toBe(0)
158+
159+
await userEvent.click(getByText('button'))
160+
await userEvent.click(getByText('toggle'))
161+
162+
await findByText('hidden')
163+
164+
resolve.splice(0).forEach((fn) => fn())
165+
await waitFor(() => expect(abortedCount).toBe(0))
166+
})
167+
168+
it('throws aborted error (like fetch)', async () => {
169+
const countAtom = atom(0)
170+
const resolve: (() => void)[] = []
171+
const derivedAtom = atom(async (get, { signal }) => {
172+
const count = get(countAtom)
173+
await new Promise<void>((r) => resolve.push(r))
174+
if (signal.aborted) {
175+
throw new Error('aborted')
176+
}
177+
return count
178+
})
179+
180+
function Component() {
181+
const count = useAtomValue(derivedAtom)
182+
return <div>count: {count}</div>
183+
}
184+
185+
function Controls() {
186+
const setCount = useSetAtom(countAtom)
187+
return <button onClick={() => setCount((c) => c + 1)}>button</button>
188+
}
189+
190+
const { findByText, getByText } = render(
191+
<StrictMode>
192+
<Suspense fallback="loading">
193+
<Component />
194+
<Controls />
195+
</Suspense>
196+
</StrictMode>,
197+
)
198+
199+
await findByText('loading')
200+
201+
resolve.splice(0).forEach((fn) => fn())
202+
await findByText('count: 0')
203+
204+
await userEvent.click(getByText('button'))
205+
await userEvent.click(getByText('button'))
206+
resolve.splice(0).forEach((fn) => fn())
207+
await findByText('count: 2')
208+
209+
await userEvent.click(getByText('button'))
210+
resolve.splice(0).forEach((fn) => fn())
211+
await findByText('count: 3')
212+
})
213+
})

0 commit comments

Comments
 (0)