diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js
index 21c8370d7c2..5358a693100 100644
--- a/packages/react-reconciler/src/ReactFiberBeginWork.js
+++ b/packages/react-reconciler/src/ReactFiberBeginWork.js
@@ -33,6 +33,7 @@ import {
MemoComponent,
SimpleMemoComponent,
LazyComponent,
+ IncompleteClassComponent,
} from 'shared/ReactWorkTags';
import {
NoEffect,
@@ -762,7 +763,7 @@ function mountLazyComponent(
renderExpirationTime,
) {
if (_current !== null) {
- // An indeterminate component only mounts if it suspended inside a non-
+ // An lazy component only mounts if it suspended inside a non-
// concurrent tree, in an inconsistent state. We want to tree it like
// a new mount, even though an empty version of it already committed.
// Disconnect the alternate pointers.
@@ -840,6 +841,64 @@ function mountLazyComponent(
return child;
}
+function mountIncompleteClassComponent(
+ _current,
+ workInProgress,
+ Component,
+ nextProps,
+ renderExpirationTime,
+) {
+ if (_current !== null) {
+ // An incomplete component only mounts if it suspended inside a non-
+ // concurrent tree, in an inconsistent state. We want to tree it like
+ // a new mount, even though an empty version of it already committed.
+ // Disconnect the alternate pointers.
+ _current.alternate = null;
+ workInProgress.alternate = null;
+ // Since this is conceptually a new fiber, schedule a Placement effect
+ workInProgress.effectTag |= Placement;
+ }
+
+ // Promote the fiber to a class and try rendering again.
+ workInProgress.tag = ClassComponent;
+
+ // The rest of this function is a fork of `updateClassComponent`
+
+ // Push context providers early to prevent context stack mismatches.
+ // During mounting we don't know the child context yet as the instance doesn't exist.
+ // We will invalidate the child context in finishClassComponent() right after rendering.
+ let hasContext;
+ if (isLegacyContextProvider(Component)) {
+ hasContext = true;
+ pushLegacyContextProvider(workInProgress);
+ } else {
+ hasContext = false;
+ }
+ prepareToReadContext(workInProgress, renderExpirationTime);
+
+ constructClassInstance(
+ workInProgress,
+ Component,
+ nextProps,
+ renderExpirationTime,
+ );
+ mountClassInstance(
+ workInProgress,
+ Component,
+ nextProps,
+ renderExpirationTime,
+ );
+
+ return finishClassComponent(
+ null,
+ workInProgress,
+ Component,
+ true,
+ hasContext,
+ renderExpirationTime,
+ );
+}
+
function mountIndeterminateComponent(
_current,
workInProgress,
@@ -1654,6 +1713,21 @@ function beginWork(
renderExpirationTime,
);
}
+ case IncompleteClassComponent: {
+ const Component = workInProgress.type;
+ const unresolvedProps = workInProgress.pendingProps;
+ const resolvedProps =
+ workInProgress.elementType === Component
+ ? unresolvedProps
+ : resolveDefaultProps(Component, unresolvedProps);
+ return mountIncompleteClassComponent(
+ current,
+ workInProgress,
+ Component,
+ resolvedProps,
+ renderExpirationTime,
+ );
+ }
default:
invariant(
false,
diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js
index b79c36f6481..1efd1fcf8df 100644
--- a/packages/react-reconciler/src/ReactFiberCommitWork.js
+++ b/packages/react-reconciler/src/ReactFiberCommitWork.js
@@ -32,6 +32,7 @@ import {
HostPortal,
Profiler,
SuspenseComponent,
+ IncompleteClassComponent,
} from 'shared/ReactWorkTags';
import {
invokeGuardedCallback,
@@ -221,6 +222,7 @@ function commitBeforeMutationLifeCycles(
case HostComponent:
case HostText:
case HostPortal:
+ case IncompleteClassComponent:
// Nothing to do for these component types
return;
default: {
@@ -392,6 +394,8 @@ function commitLifeCycles(
}
return;
}
+ case IncompleteClassComponent:
+ break;
default: {
invariant(
false,
@@ -495,7 +499,13 @@ function commitUnmount(current: Fiber): void {
case ClassComponent: {
safelyDetachRef(current);
const instance = current.stateNode;
- if (typeof instance.componentWillUnmount === 'function') {
+ if (
+ // Typically, a component that mounted will have an instance. However,
+ // outside of concurrent mode, a suspended component may commit without
+ // an instance, so we need to check whether it exists.
+ instance !== null &&
+ typeof instance.componentWillUnmount === 'function'
+ ) {
safelyCallComponentWillUnmount(current, instance);
}
return;
@@ -924,6 +934,9 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
case SuspenseComponent: {
return;
}
+ case IncompleteClassComponent: {
+ return;
+ }
default: {
invariant(
false,
diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js
index c0657661a12..79c36c1a774 100644
--- a/packages/react-reconciler/src/ReactFiberCompleteWork.js
+++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js
@@ -37,6 +37,7 @@ import {
MemoComponent,
SimpleMemoComponent,
LazyComponent,
+ IncompleteClassComponent,
} from 'shared/ReactWorkTags';
import {Placement, Ref, Update} from 'shared/ReactSideEffectTags';
import invariant from 'shared/invariant';
@@ -717,6 +718,15 @@ function completeWork(
break;
case MemoComponent:
break;
+ case IncompleteClassComponent: {
+ // Same as class component case. I put it down here so that the tags are
+ // sequential to ensure this switch is compiled to a jump table.
+ const Component = workInProgress.type;
+ if (isLegacyContextProvider(Component)) {
+ popLegacyContext(workInProgress);
+ }
+ break;
+ }
default:
invariant(
false,
diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.js b/packages/react-reconciler/src/ReactFiberUnwindWork.js
index 44f5a20ee84..40c814a37d7 100644
--- a/packages/react-reconciler/src/ReactFiberUnwindWork.js
+++ b/packages/react-reconciler/src/ReactFiberUnwindWork.js
@@ -25,6 +25,7 @@ import {
HostPortal,
ContextProvider,
SuspenseComponent,
+ IncompleteClassComponent,
} from 'shared/ReactWorkTags';
import {
DidCapture,
@@ -252,10 +253,12 @@ function throwException(
// But we shouldn't call any lifecycle methods or callbacks. Remove
// all lifecycle effect tags.
sourceFiber.effectTag &= ~LifecycleEffectMask;
- if (sourceFiber.alternate === null) {
- // Set the instance back to null. We use this as a heuristic to
- // detect that the fiber mounted in an inconsistent state.
- sourceFiber.stateNode = null;
+ const current = sourceFiber.alternate;
+ if (current === null) {
+ // This is a new mount. Change the tag so it's not mistaken for a
+ // completed component. For example, we should not call
+ // componentWillUnmount if it is deleted.
+ sourceFiber.tag = IncompleteClassComponent;
}
}
diff --git a/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js
index 8362ee523d2..305b7408745 100644
--- a/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js
@@ -492,6 +492,45 @@ describe('ReactSuspense', () => {
expect(root).toMatchRenderedOutput('Stateful: 2B');
});
+ it('suspends in a class that has componentWillUnmount and is then deleted', () => {
+ class AsyncTextWithUnmount extends React.Component {
+ componentWillUnmount() {
+ ReactTestRenderer.unstable_yield('will unmount');
+ }
+ render() {
+ const text = this.props.text;
+ const ms = this.props.ms;
+ try {
+ TextResource.read(cache, [text, ms]);
+ ReactTestRenderer.unstable_yield(text);
+ return text;
+ } catch (promise) {
+ if (typeof promise.then === 'function') {
+ ReactTestRenderer.unstable_yield(`Suspend! [${text}]`);
+ } else {
+ ReactTestRenderer.unstable_yield(`Error! [${text}]`);
+ }
+ throw promise;
+ }
+ }
+ }
+
+ function App({text}) {
+ return (
+ }>
+
+
+ );
+ }
+
+ const root = ReactTestRenderer.create();
+ expect(ReactTestRenderer).toHaveYielded(['Suspend! [A]', 'Loading...']);
+ root.update();
+ // Should not fire componentWillUnmount
+ expect(ReactTestRenderer).toHaveYielded(['B']);
+ expect(root).toMatchRenderedOutput('B');
+ });
+
it('retries when an update is scheduled on a timed out tree', () => {
let instance;
class Stateful extends React.Component {
diff --git a/packages/react-test-renderer/src/ReactTestRenderer.js b/packages/react-test-renderer/src/ReactTestRenderer.js
index 6a8996c609c..21f007525f4 100644
--- a/packages/react-test-renderer/src/ReactTestRenderer.js
+++ b/packages/react-test-renderer/src/ReactTestRenderer.js
@@ -29,6 +29,7 @@ import {
Profiler,
MemoComponent,
SimpleMemoComponent,
+ IncompleteClassComponent,
} from 'shared/ReactWorkTags';
import invariant from 'shared/invariant';
import ReactVersion from 'shared/ReactVersion';
@@ -193,6 +194,7 @@ function toTree(node: ?Fiber) {
case Profiler:
case ForwardRef:
case MemoComponent:
+ case IncompleteClassComponent:
return childrenToTree(node.child);
default:
invariant(
diff --git a/packages/shared/ReactWorkTags.js b/packages/shared/ReactWorkTags.js
index bb7bee12d9d..aed81502179 100644
--- a/packages/shared/ReactWorkTags.js
+++ b/packages/shared/ReactWorkTags.js
@@ -45,3 +45,4 @@ export const SuspenseComponent = 13;
export const MemoComponent = 14;
export const SimpleMemoComponent = 15;
export const LazyComponent = 16;
+export const IncompleteClassComponent = 17;