Skip to content

Commit

Permalink
Skip unmounted error boundaries for errors thrown during ref cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
Brian Vaughn committed Aug 17, 2020
1 parent a994a53 commit a048c85
Show file tree
Hide file tree
Showing 3 changed files with 98 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2558,4 +2558,90 @@ describe('ReactErrorBoundaries', () => {
'Component render OuterFallback',
]);
});

// @gate skipUnmountedBoundaries
it('catches errors thrown while detaching refs', () => {
class LocalErrorBoundary extends React.Component {
state = {error: null};
static getDerivedStateFromError(error) {
Scheduler.unstable_yieldValue(
`ErrorBoundary static getDerivedStateFromError`,
);
return {error};
}
render() {
const {children, id, fallbackID} = this.props;
const {error} = this.state;
if (error) {
Scheduler.unstable_yieldValue(`${id} render error`);
return <Component id={fallbackID} />;
}
Scheduler.unstable_yieldValue(`${id} render success`);
return children || null;
}
}

class Component extends React.Component {
render() {
const {id} = this.props;
Scheduler.unstable_yieldValue('Component render ' + id);
return id;
}
}

class LocalBrokenCallbackRef extends React.Component {
_ref = ref => {
Scheduler.unstable_yieldValue('LocalBrokenCallbackRef ref ' + !!ref);
if (ref === null) {
throw Error('Expected');
}
};

render() {
Scheduler.unstable_yieldValue('LocalBrokenCallbackRef render');
return <div ref={this._ref}>ref</div>;
}
}

const container = document.createElement('div');

ReactDOM.render(
<LocalErrorBoundary id="OuterBoundary" fallbackID="OuterFallback">
<Component id="sibling" />
<LocalErrorBoundary id="InnerBoundary" fallbackID="InnerFallback">
<LocalBrokenCallbackRef />
</LocalErrorBoundary>
</LocalErrorBoundary>,
container,
);

expect(container.firstChild.textContent).toBe('sibling');
expect(container.lastChild.textContent).toBe('ref');
expect(Scheduler).toHaveYielded([
'OuterBoundary render success',
'Component render sibling',
'InnerBoundary render success',
'LocalBrokenCallbackRef render',
'LocalBrokenCallbackRef ref true',
]);

ReactDOM.render(
<LocalErrorBoundary id="OuterBoundary" fallbackID="OuterFallback">
<Component id="sibling" />
</LocalErrorBoundary>,
container,
);

// React should skip over the unmounting boundary and find the nearest still-mounted boundary.
expect(container.firstChild.textContent).toBe('OuterFallback');
expect(container.lastChild.textContent).toBe('OuterFallback');
expect(Scheduler).toHaveYielded([
'OuterBoundary render success',
'Component render sibling',
'LocalBrokenCallbackRef ref false',
'ErrorBoundary static getDerivedStateFromError',
'OuterBoundary render error',
'Component render OuterFallback',
]);
});
});
12 changes: 6 additions & 6 deletions packages/react-reconciler/src/ReactFiberCommitWork.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,21 +188,21 @@ function safelyCallComponentWillUnmount(
}
}

function safelyDetachRef(current: Fiber) {
function safelyDetachRef(current: Fiber, nearestMountedAncestor: Fiber) {
const ref = current.ref;
if (ref !== null) {
if (typeof ref === 'function') {
if (__DEV__) {
invokeGuardedCallback(null, ref, null, null);
if (hasCaughtError()) {
const refError = clearCaughtError();
captureCommitPhaseError(current, current.return, refError);
captureCommitPhaseError(current, nearestMountedAncestor, refError);
}
} else {
try {
ref(null);
} catch (refError) {
captureCommitPhaseError(current, current.return, refError);
captureCommitPhaseError(current, nearestMountedAncestor, refError);
}
}
} else {
Expand Down Expand Up @@ -1020,7 +1020,7 @@ function commitUnmount(
return;
}
case ClassComponent: {
safelyDetachRef(current);
safelyDetachRef(current, nearestMountedAncestor);
const instance = current.stateNode;
if (typeof instance.componentWillUnmount === 'function') {
safelyCallComponentWillUnmount(
Expand All @@ -1032,7 +1032,7 @@ function commitUnmount(
return;
}
case HostComponent: {
safelyDetachRef(current);
safelyDetachRef(current, nearestMountedAncestor);
return;
}
case HostPortal: {
Expand Down Expand Up @@ -1075,7 +1075,7 @@ function commitUnmount(
}
case ScopeComponent: {
if (enableScopeAPI) {
safelyDetachRef(current);
safelyDetachRef(current, nearestMountedAncestor);
}
return;
}
Expand Down
12 changes: 6 additions & 6 deletions packages/react-reconciler/src/ReactFiberCommitWork.old.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,21 +179,21 @@ function safelyCallComponentWillUnmount(
}
}

function safelyDetachRef(current: Fiber) {
function safelyDetachRef(current: Fiber, nearestMountedAncestor: Fiber | null) {
const ref = current.ref;
if (ref !== null) {
if (typeof ref === 'function') {
if (__DEV__) {
invokeGuardedCallback(null, ref, null, null);
if (hasCaughtError()) {
const refError = clearCaughtError();
captureCommitPhaseError(current, current.return, refError);
captureCommitPhaseError(current, nearestMountedAncestor, refError);
}
} else {
try {
ref(null);
} catch (refError) {
captureCommitPhaseError(current, current.return, refError);
captureCommitPhaseError(current, nearestMountedAncestor, refError);
}
}
} else {
Expand Down Expand Up @@ -918,7 +918,7 @@ function commitUnmount(
return;
}
case ClassComponent: {
safelyDetachRef(current);
safelyDetachRef(current, nearestMountedAncestor);
const instance = current.stateNode;
if (typeof instance.componentWillUnmount === 'function') {
safelyCallComponentWillUnmount(
Expand All @@ -930,7 +930,7 @@ function commitUnmount(
return;
}
case HostComponent: {
safelyDetachRef(current);
safelyDetachRef(current, nearestMountedAncestor);
return;
}
case HostPortal: {
Expand Down Expand Up @@ -973,7 +973,7 @@ function commitUnmount(
}
case ScopeComponent: {
if (enableScopeAPI) {
safelyDetachRef(current);
safelyDetachRef(current, nearestMountedAncestor);
}
return;
}
Expand Down

0 comments on commit a048c85

Please sign in to comment.