Skip to content

Commit cfb0345

Browse files
authored
Merge pull request #35 from mpeyper/suspense-support
Add Suspense and ErrorBoundary to better support suspending from hooks Fixes #27
2 parents 77c3cbf + f5914f4 commit cfb0345

File tree

2 files changed

+97
-14
lines changed

2 files changed

+97
-14
lines changed

src/index.js

+46-14
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,37 @@
1-
import React from 'react'
1+
import React, { Suspense } from 'react'
22
import { render, cleanup, act } from 'react-testing-library'
33

44
function TestHook({ callback, hookProps, children }) {
5-
try {
6-
children(callback(hookProps))
7-
} catch (e) {
8-
children(undefined, e)
5+
children(callback(hookProps))
6+
return null
7+
}
8+
9+
class ErrorBoundary extends React.Component {
10+
constructor(props) {
11+
super(props)
12+
this.state = { hasError: false }
13+
}
14+
15+
static getDerivedStateFromError() {
16+
return { hasError: true }
917
}
18+
19+
componentDidCatch(error) {
20+
this.props.onError(error)
21+
}
22+
23+
componentDidUpdate(prevProps) {
24+
if (this.props != prevProps && this.state.hasError) {
25+
this.setState({ hasError: false })
26+
}
27+
}
28+
29+
render() {
30+
return !this.state.hasError && this.props.children
31+
}
32+
}
33+
34+
function Fallback() {
1035
return null
1136
}
1237

@@ -27,27 +52,34 @@ function resultContainer() {
2752
}
2853
}
2954

55+
const updateResult = (val, err) => {
56+
value = val
57+
error = err
58+
resolvers.splice(0, resolvers.length).forEach((resolve) => resolve())
59+
}
60+
3061
return {
3162
result,
3263
addResolver: (resolver) => {
3364
resolvers.push(resolver)
3465
},
35-
updateResult: (val, err) => {
36-
value = val
37-
error = err
38-
resolvers.splice(0, resolvers.length).forEach((resolve) => resolve())
39-
}
66+
setValue: (val) => updateResult(val),
67+
setError: (err) => updateResult(undefined, err)
4068
}
4169
}
4270

4371
function renderHook(callback, { initialProps, ...options } = {}) {
44-
const { result, updateResult, addResolver } = resultContainer()
72+
const { result, setValue, setError, addResolver } = resultContainer()
4573
const hookProps = { current: initialProps }
4674

4775
const toRender = () => (
48-
<TestHook callback={callback} hookProps={hookProps.current}>
49-
{updateResult}
50-
</TestHook>
76+
<ErrorBoundary onError={setError}>
77+
<Suspense fallback={<Fallback />}>
78+
<TestHook callback={callback} hookProps={hookProps.current}>
79+
{setValue}
80+
</TestHook>
81+
</Suspense>
82+
</ErrorBoundary>
5183
)
5284

5385
const { unmount, rerender: rerenderComponent } = render(toRender(), options)

test/suspenseHook.test.js

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { renderHook, cleanup } from 'src'
2+
3+
describe('suspense hook tests', () => {
4+
const cache = {}
5+
const fetchName = (isSuccessful) => {
6+
if (!cache.value) {
7+
cache.value = new Promise((resolve, reject) => {
8+
setTimeout(() => {
9+
if (isSuccessful) {
10+
resolve('Bob')
11+
} else {
12+
reject(new Error('Failed to fetch name'))
13+
}
14+
}, 50)
15+
})
16+
.then((value) => (cache.value = value))
17+
.catch((e) => (cache.value = e))
18+
}
19+
return cache.value
20+
}
21+
22+
const useFetchName = (isSuccessful = true) => {
23+
const name = fetchName(isSuccessful)
24+
if (typeof name.then === 'function' || name instanceof Error) {
25+
throw name
26+
}
27+
return name
28+
}
29+
30+
beforeEach(() => {
31+
delete cache.value
32+
})
33+
34+
afterEach(cleanup)
35+
36+
test('should allow rendering to be suspended', async () => {
37+
const { result, waitForNextUpdate } = renderHook(() => useFetchName(true))
38+
39+
await waitForNextUpdate()
40+
41+
expect(result.current).toBe('Bob')
42+
})
43+
44+
test('should set error if suspense promise rejects', async () => {
45+
const { result, waitForNextUpdate } = renderHook(() => useFetchName(false))
46+
47+
await waitForNextUpdate()
48+
49+
expect(result.error).toEqual(new Error('Failed to fetch name'))
50+
})
51+
})

0 commit comments

Comments
 (0)