Skip to content

Commit bcbe951

Browse files
kittenkhmm12
andauthored
Fix Dispatcher reset edge cases (#38)
* WIP Ensure that Dispatcher is always reset correctly * Refactor and fix test execution * Add test for exceptions handling inside yield Co-authored-by: Maxim <khmm12@gmail.com>
1 parent 3dce942 commit bcbe951

File tree

3 files changed

+77
-47
lines changed

3 files changed

+77
-47
lines changed

src/__tests__/suspense.test.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,27 @@ describe('renderPrepass', () => {
3737
})
3838
})
3939

40+
it('rejects promise after getting of exception inside', () => {
41+
const exception = new TypeError('Something went wrong')
42+
43+
const Inner = jest.fn(() => {
44+
throw exception
45+
})
46+
47+
const Outer = () => {
48+
const start = Date.now()
49+
while (Date.now() - start < 40) {}
50+
return <Inner />
51+
}
52+
53+
const render$ = renderPrepass(<Outer />)
54+
55+
return render$.catch(error => {
56+
expect(Inner).toHaveBeenCalledTimes(1)
57+
expect(error).toBe(exception)
58+
})
59+
})
60+
4061
it('preserves the correct legacy context values across yields', () => {
4162
let called = false
4263
const Inner = (_, context) => {

src/index.js

Lines changed: 40 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,9 @@
22

33
import React, { type Node, type Element } from 'react'
44
import type { Visitor, YieldFrame, Frame, AbstractElement } from './types'
5-
import { visitChildren, resumeVisitChildren } from './visitor'
5+
import { visitChildren, resumeVisitChildren, update } from './visitor'
66
import { getChildrenArray } from './element'
77

8-
import {
9-
updateFunctionComponent,
10-
updateClassComponent,
11-
updateLazyComponent
12-
} from './render'
13-
148
import {
159
setCurrentContextStore,
1610
setCurrentContextMap,
@@ -21,7 +15,29 @@ const {
2115
ReactCurrentDispatcher
2216
} = (React: any).__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
2317

24-
let prevDispatcher = ReactCurrentDispatcher.current
18+
/** wrapWithDispatcher accepts a function and wraps it
19+
in one that sets up our ReactCurrentDispatcher and
20+
resets it afterwards */
21+
function wrapWithDispatcher<T: Function>(exec: T): T {
22+
// $FlowFixMe
23+
return (...args) => {
24+
const prevDispatcher = ReactCurrentDispatcher.current
25+
26+
try {
27+
// The "Dispatcher" is what handles hook calls and
28+
// a React internal that needs to be set to our dispatcher
29+
ReactCurrentDispatcher.current = Dispatcher
30+
return exec(...args)
31+
} finally {
32+
// We're resetting the dispatcher after we're done
33+
ReactCurrentDispatcher.current = prevDispatcher
34+
}
35+
}
36+
}
37+
38+
const resumeWithDispatcher = wrapWithDispatcher(resumeVisitChildren)
39+
const visitWithDispatcher = wrapWithDispatcher(visitChildren)
40+
const updateWithDispatcher = wrapWithDispatcher(update)
2541

2642
/** visitChildren walks all elements (depth-first) and while it walks the
2743
element tree some components will suspend and put a `Frame` onto
@@ -34,50 +50,35 @@ const updateWithFrame = (
3450
visitor: Visitor
3551
): Promise<void> => {
3652
if (frame.kind === 'frame.yield') {
37-
const yieldFrame: YieldFrame = frame
38-
39-
return new Promise(resolve => {
53+
return new Promise((resolve, reject) => {
4054
setImmediate(() => {
41-
prevDispatcher = ReactCurrentDispatcher.current
42-
ReactCurrentDispatcher.current = Dispatcher
43-
resumeVisitChildren(yieldFrame, queue, visitor)
44-
ReactCurrentDispatcher.current = prevDispatcher
45-
resolve()
55+
try {
56+
resumeWithDispatcher(frame, queue, visitor)
57+
resolve()
58+
} catch (err) {
59+
reject(err)
60+
}
4661
})
4762
})
4863
}
4964

5065
return frame.thenable.then(() => {
51-
prevDispatcher = ReactCurrentDispatcher.current
52-
ReactCurrentDispatcher.current = Dispatcher
53-
54-
let children = []
55-
5666
// Update the component after we've suspended to rerender it,
5767
// at which point we'll actually get its children
58-
if (frame.kind === 'frame.class') {
59-
children = updateClassComponent(queue, frame)
60-
} else if (frame.kind === 'frame.hooks') {
61-
children = updateFunctionComponent(queue, frame)
62-
} else if (frame.kind === 'frame.lazy') {
63-
children = updateLazyComponent(queue, frame)
64-
}
65-
68+
const children = updateWithDispatcher(frame, queue)
6669
// Now continue walking the previously suspended component's
6770
// children (which might also suspend)
68-
visitChildren(getChildrenArray(children), queue, visitor)
69-
ReactCurrentDispatcher.current = prevDispatcher
71+
visitWithDispatcher(getChildrenArray(children), queue, visitor)
7072
})
7173
}
7274

7375
const flushFrames = (queue: Frame[], visitor: Visitor): Promise<void> => {
74-
if (queue.length === 0) {
75-
return Promise.resolve()
76-
}
77-
78-
return updateWithFrame(queue.shift(), queue, visitor).then(() =>
79-
flushFrames(queue, visitor)
80-
)
76+
const frame = queue.shift()
77+
return frame
78+
? updateWithFrame(frame, queue, visitor).then(() =>
79+
flushFrames(queue, visitor)
80+
)
81+
: Promise.resolve()
8182
}
8283

8384
const defaultVisitor = () => undefined
@@ -93,17 +94,9 @@ const renderPrepass = (element: Node, visitor?: Visitor): Promise<void> => {
9394
setCurrentContextStore(new Map())
9495

9596
try {
96-
// The "Dispatcher" is what handles hook calls and
97-
// a React internal that needs to be set to our
98-
// dispatcher and reset after we're done
99-
prevDispatcher = ReactCurrentDispatcher.current
100-
ReactCurrentDispatcher.current = Dispatcher
101-
102-
visitChildren(getChildrenArray(element), queue, fn)
97+
visitWithDispatcher(getChildrenArray(element), queue, fn)
10398
} catch (error) {
10499
return Promise.reject(error)
105-
} finally {
106-
ReactCurrentDispatcher.current = prevDispatcher
107100
}
108101

109102
return flushFrames(queue, fn)

src/visitor.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@ import {
1111

1212
import {
1313
mountFunctionComponent,
14+
updateFunctionComponent,
1415
mountClassComponent,
16+
updateClassComponent,
1517
mountLazyComponent,
18+
updateLazyComponent,
1619
mountStyledComponent,
1720
isStyledElement
1821
} from './render'
@@ -263,3 +266,16 @@ export const resumeVisitChildren = (
263266
queue.unshift(makeYieldFrame(frame.children, frame.map, frame.store))
264267
}
265268
}
269+
270+
export const update = (frame: Frame, queue: Frame[]): Node => {
271+
switch (frame.kind) {
272+
case 'frame.class':
273+
return updateClassComponent(queue, frame)
274+
case 'frame.hooks':
275+
return updateFunctionComponent(queue, frame)
276+
case 'frame.lazy':
277+
return updateLazyComponent(queue, frame)
278+
default:
279+
return []
280+
}
281+
}

0 commit comments

Comments
 (0)