Skip to content

Commit a048c85

Browse files
author
Brian Vaughn
committed
Skip unmounted error boundaries for errors thrown during ref cleanup
1 parent a994a53 commit a048c85

File tree

3 files changed

+98
-12
lines changed

3 files changed

+98
-12
lines changed

packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2558,4 +2558,90 @@ describe('ReactErrorBoundaries', () => {
25582558
'Component render OuterFallback',
25592559
]);
25602560
});
2561+
2562+
// @gate skipUnmountedBoundaries
2563+
it('catches errors thrown while detaching refs', () => {
2564+
class LocalErrorBoundary extends React.Component {
2565+
state = {error: null};
2566+
static getDerivedStateFromError(error) {
2567+
Scheduler.unstable_yieldValue(
2568+
`ErrorBoundary static getDerivedStateFromError`,
2569+
);
2570+
return {error};
2571+
}
2572+
render() {
2573+
const {children, id, fallbackID} = this.props;
2574+
const {error} = this.state;
2575+
if (error) {
2576+
Scheduler.unstable_yieldValue(`${id} render error`);
2577+
return <Component id={fallbackID} />;
2578+
}
2579+
Scheduler.unstable_yieldValue(`${id} render success`);
2580+
return children || null;
2581+
}
2582+
}
2583+
2584+
class Component extends React.Component {
2585+
render() {
2586+
const {id} = this.props;
2587+
Scheduler.unstable_yieldValue('Component render ' + id);
2588+
return id;
2589+
}
2590+
}
2591+
2592+
class LocalBrokenCallbackRef extends React.Component {
2593+
_ref = ref => {
2594+
Scheduler.unstable_yieldValue('LocalBrokenCallbackRef ref ' + !!ref);
2595+
if (ref === null) {
2596+
throw Error('Expected');
2597+
}
2598+
};
2599+
2600+
render() {
2601+
Scheduler.unstable_yieldValue('LocalBrokenCallbackRef render');
2602+
return <div ref={this._ref}>ref</div>;
2603+
}
2604+
}
2605+
2606+
const container = document.createElement('div');
2607+
2608+
ReactDOM.render(
2609+
<LocalErrorBoundary id="OuterBoundary" fallbackID="OuterFallback">
2610+
<Component id="sibling" />
2611+
<LocalErrorBoundary id="InnerBoundary" fallbackID="InnerFallback">
2612+
<LocalBrokenCallbackRef />
2613+
</LocalErrorBoundary>
2614+
</LocalErrorBoundary>,
2615+
container,
2616+
);
2617+
2618+
expect(container.firstChild.textContent).toBe('sibling');
2619+
expect(container.lastChild.textContent).toBe('ref');
2620+
expect(Scheduler).toHaveYielded([
2621+
'OuterBoundary render success',
2622+
'Component render sibling',
2623+
'InnerBoundary render success',
2624+
'LocalBrokenCallbackRef render',
2625+
'LocalBrokenCallbackRef ref true',
2626+
]);
2627+
2628+
ReactDOM.render(
2629+
<LocalErrorBoundary id="OuterBoundary" fallbackID="OuterFallback">
2630+
<Component id="sibling" />
2631+
</LocalErrorBoundary>,
2632+
container,
2633+
);
2634+
2635+
// React should skip over the unmounting boundary and find the nearest still-mounted boundary.
2636+
expect(container.firstChild.textContent).toBe('OuterFallback');
2637+
expect(container.lastChild.textContent).toBe('OuterFallback');
2638+
expect(Scheduler).toHaveYielded([
2639+
'OuterBoundary render success',
2640+
'Component render sibling',
2641+
'LocalBrokenCallbackRef ref false',
2642+
'ErrorBoundary static getDerivedStateFromError',
2643+
'OuterBoundary render error',
2644+
'Component render OuterFallback',
2645+
]);
2646+
});
25612647
});

packages/react-reconciler/src/ReactFiberCommitWork.new.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -188,21 +188,21 @@ function safelyCallComponentWillUnmount(
188188
}
189189
}
190190

191-
function safelyDetachRef(current: Fiber) {
191+
function safelyDetachRef(current: Fiber, nearestMountedAncestor: Fiber) {
192192
const ref = current.ref;
193193
if (ref !== null) {
194194
if (typeof ref === 'function') {
195195
if (__DEV__) {
196196
invokeGuardedCallback(null, ref, null, null);
197197
if (hasCaughtError()) {
198198
const refError = clearCaughtError();
199-
captureCommitPhaseError(current, current.return, refError);
199+
captureCommitPhaseError(current, nearestMountedAncestor, refError);
200200
}
201201
} else {
202202
try {
203203
ref(null);
204204
} catch (refError) {
205-
captureCommitPhaseError(current, current.return, refError);
205+
captureCommitPhaseError(current, nearestMountedAncestor, refError);
206206
}
207207
}
208208
} else {
@@ -1020,7 +1020,7 @@ function commitUnmount(
10201020
return;
10211021
}
10221022
case ClassComponent: {
1023-
safelyDetachRef(current);
1023+
safelyDetachRef(current, nearestMountedAncestor);
10241024
const instance = current.stateNode;
10251025
if (typeof instance.componentWillUnmount === 'function') {
10261026
safelyCallComponentWillUnmount(
@@ -1032,7 +1032,7 @@ function commitUnmount(
10321032
return;
10331033
}
10341034
case HostComponent: {
1035-
safelyDetachRef(current);
1035+
safelyDetachRef(current, nearestMountedAncestor);
10361036
return;
10371037
}
10381038
case HostPortal: {
@@ -1075,7 +1075,7 @@ function commitUnmount(
10751075
}
10761076
case ScopeComponent: {
10771077
if (enableScopeAPI) {
1078-
safelyDetachRef(current);
1078+
safelyDetachRef(current, nearestMountedAncestor);
10791079
}
10801080
return;
10811081
}

packages/react-reconciler/src/ReactFiberCommitWork.old.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -179,21 +179,21 @@ function safelyCallComponentWillUnmount(
179179
}
180180
}
181181

182-
function safelyDetachRef(current: Fiber) {
182+
function safelyDetachRef(current: Fiber, nearestMountedAncestor: Fiber | null) {
183183
const ref = current.ref;
184184
if (ref !== null) {
185185
if (typeof ref === 'function') {
186186
if (__DEV__) {
187187
invokeGuardedCallback(null, ref, null, null);
188188
if (hasCaughtError()) {
189189
const refError = clearCaughtError();
190-
captureCommitPhaseError(current, current.return, refError);
190+
captureCommitPhaseError(current, nearestMountedAncestor, refError);
191191
}
192192
} else {
193193
try {
194194
ref(null);
195195
} catch (refError) {
196-
captureCommitPhaseError(current, current.return, refError);
196+
captureCommitPhaseError(current, nearestMountedAncestor, refError);
197197
}
198198
}
199199
} else {
@@ -918,7 +918,7 @@ function commitUnmount(
918918
return;
919919
}
920920
case ClassComponent: {
921-
safelyDetachRef(current);
921+
safelyDetachRef(current, nearestMountedAncestor);
922922
const instance = current.stateNode;
923923
if (typeof instance.componentWillUnmount === 'function') {
924924
safelyCallComponentWillUnmount(
@@ -930,7 +930,7 @@ function commitUnmount(
930930
return;
931931
}
932932
case HostComponent: {
933-
safelyDetachRef(current);
933+
safelyDetachRef(current, nearestMountedAncestor);
934934
return;
935935
}
936936
case HostPortal: {
@@ -973,7 +973,7 @@ function commitUnmount(
973973
}
974974
case ScopeComponent: {
975975
if (enableScopeAPI) {
976-
safelyDetachRef(current);
976+
safelyDetachRef(current, nearestMountedAncestor);
977977
}
978978
return;
979979
}

0 commit comments

Comments
 (0)