Skip to content

Commit a71c613

Browse files
acdlitejetoneza
authored andcommitted
[Synchronous Suspense] Suspending a class outside concurrent mode (facebook#13926)
* [Synchronous Suspense] Suspending a class outside concurrent mode When a class component suspends during mount outside concurrent mode, change the tag so it's not mistaken for a completed component. For example, we should not call componentWillUnmount if it is deleted. * PR nits
1 parent 16ca00c commit a71c613

File tree

7 files changed

+148
-6
lines changed

7 files changed

+148
-6
lines changed

packages/react-reconciler/src/ReactFiberBeginWork.js

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
MemoComponent,
3434
SimpleMemoComponent,
3535
LazyComponent,
36+
IncompleteClassComponent,
3637
} from 'shared/ReactWorkTags';
3738
import {
3839
NoEffect,
@@ -762,7 +763,7 @@ function mountLazyComponent(
762763
renderExpirationTime,
763764
) {
764765
if (_current !== null) {
765-
// An indeterminate component only mounts if it suspended inside a non-
766+
// An lazy component only mounts if it suspended inside a non-
766767
// concurrent tree, in an inconsistent state. We want to tree it like
767768
// a new mount, even though an empty version of it already committed.
768769
// Disconnect the alternate pointers.
@@ -840,6 +841,64 @@ function mountLazyComponent(
840841
return child;
841842
}
842843

844+
function mountIncompleteClassComponent(
845+
_current,
846+
workInProgress,
847+
Component,
848+
nextProps,
849+
renderExpirationTime,
850+
) {
851+
if (_current !== null) {
852+
// An incomplete component only mounts if it suspended inside a non-
853+
// concurrent tree, in an inconsistent state. We want to tree it like
854+
// a new mount, even though an empty version of it already committed.
855+
// Disconnect the alternate pointers.
856+
_current.alternate = null;
857+
workInProgress.alternate = null;
858+
// Since this is conceptually a new fiber, schedule a Placement effect
859+
workInProgress.effectTag |= Placement;
860+
}
861+
862+
// Promote the fiber to a class and try rendering again.
863+
workInProgress.tag = ClassComponent;
864+
865+
// The rest of this function is a fork of `updateClassComponent`
866+
867+
// Push context providers early to prevent context stack mismatches.
868+
// During mounting we don't know the child context yet as the instance doesn't exist.
869+
// We will invalidate the child context in finishClassComponent() right after rendering.
870+
let hasContext;
871+
if (isLegacyContextProvider(Component)) {
872+
hasContext = true;
873+
pushLegacyContextProvider(workInProgress);
874+
} else {
875+
hasContext = false;
876+
}
877+
prepareToReadContext(workInProgress, renderExpirationTime);
878+
879+
constructClassInstance(
880+
workInProgress,
881+
Component,
882+
nextProps,
883+
renderExpirationTime,
884+
);
885+
mountClassInstance(
886+
workInProgress,
887+
Component,
888+
nextProps,
889+
renderExpirationTime,
890+
);
891+
892+
return finishClassComponent(
893+
null,
894+
workInProgress,
895+
Component,
896+
true,
897+
hasContext,
898+
renderExpirationTime,
899+
);
900+
}
901+
843902
function mountIndeterminateComponent(
844903
_current,
845904
workInProgress,
@@ -1654,6 +1713,21 @@ function beginWork(
16541713
renderExpirationTime,
16551714
);
16561715
}
1716+
case IncompleteClassComponent: {
1717+
const Component = workInProgress.type;
1718+
const unresolvedProps = workInProgress.pendingProps;
1719+
const resolvedProps =
1720+
workInProgress.elementType === Component
1721+
? unresolvedProps
1722+
: resolveDefaultProps(Component, unresolvedProps);
1723+
return mountIncompleteClassComponent(
1724+
current,
1725+
workInProgress,
1726+
Component,
1727+
resolvedProps,
1728+
renderExpirationTime,
1729+
);
1730+
}
16571731
default:
16581732
invariant(
16591733
false,

packages/react-reconciler/src/ReactFiberCommitWork.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
HostPortal,
3333
Profiler,
3434
SuspenseComponent,
35+
IncompleteClassComponent,
3536
} from 'shared/ReactWorkTags';
3637
import {
3738
invokeGuardedCallback,
@@ -221,6 +222,7 @@ function commitBeforeMutationLifeCycles(
221222
case HostComponent:
222223
case HostText:
223224
case HostPortal:
225+
case IncompleteClassComponent:
224226
// Nothing to do for these component types
225227
return;
226228
default: {
@@ -392,6 +394,8 @@ function commitLifeCycles(
392394
}
393395
return;
394396
}
397+
case IncompleteClassComponent:
398+
break;
395399
default: {
396400
invariant(
397401
false,
@@ -495,7 +499,13 @@ function commitUnmount(current: Fiber): void {
495499
case ClassComponent: {
496500
safelyDetachRef(current);
497501
const instance = current.stateNode;
498-
if (typeof instance.componentWillUnmount === 'function') {
502+
if (
503+
// Typically, a component that mounted will have an instance. However,
504+
// outside of concurrent mode, a suspended component may commit without
505+
// an instance, so we need to check whether it exists.
506+
instance !== null &&
507+
typeof instance.componentWillUnmount === 'function'
508+
) {
499509
safelyCallComponentWillUnmount(current, instance);
500510
}
501511
return;
@@ -924,6 +934,9 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
924934
case SuspenseComponent: {
925935
return;
926936
}
937+
case IncompleteClassComponent: {
938+
return;
939+
}
927940
default: {
928941
invariant(
929942
false,

packages/react-reconciler/src/ReactFiberCompleteWork.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
MemoComponent,
3838
SimpleMemoComponent,
3939
LazyComponent,
40+
IncompleteClassComponent,
4041
} from 'shared/ReactWorkTags';
4142
import {Placement, Ref, Update} from 'shared/ReactSideEffectTags';
4243
import invariant from 'shared/invariant';
@@ -717,6 +718,15 @@ function completeWork(
717718
break;
718719
case MemoComponent:
719720
break;
721+
case IncompleteClassComponent: {
722+
// Same as class component case. I put it down here so that the tags are
723+
// sequential to ensure this switch is compiled to a jump table.
724+
const Component = workInProgress.type;
725+
if (isLegacyContextProvider(Component)) {
726+
popLegacyContext(workInProgress);
727+
}
728+
break;
729+
}
720730
default:
721731
invariant(
722732
false,

packages/react-reconciler/src/ReactFiberUnwindWork.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
HostPortal,
2626
ContextProvider,
2727
SuspenseComponent,
28+
IncompleteClassComponent,
2829
} from 'shared/ReactWorkTags';
2930
import {
3031
DidCapture,
@@ -252,10 +253,12 @@ function throwException(
252253
// But we shouldn't call any lifecycle methods or callbacks. Remove
253254
// all lifecycle effect tags.
254255
sourceFiber.effectTag &= ~LifecycleEffectMask;
255-
if (sourceFiber.alternate === null) {
256-
// Set the instance back to null. We use this as a heuristic to
257-
// detect that the fiber mounted in an inconsistent state.
258-
sourceFiber.stateNode = null;
256+
const current = sourceFiber.alternate;
257+
if (current === null) {
258+
// This is a new mount. Change the tag so it's not mistaken for a
259+
// completed component. For example, we should not call
260+
// componentWillUnmount if it is deleted.
261+
sourceFiber.tag = IncompleteClassComponent;
259262
}
260263
}
261264

packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,45 @@ describe('ReactSuspense', () => {
492492
expect(root).toMatchRenderedOutput('Stateful: 2B');
493493
});
494494

495+
it('suspends in a class that has componentWillUnmount and is then deleted', () => {
496+
class AsyncTextWithUnmount extends React.Component {
497+
componentWillUnmount() {
498+
ReactTestRenderer.unstable_yield('will unmount');
499+
}
500+
render() {
501+
const text = this.props.text;
502+
const ms = this.props.ms;
503+
try {
504+
TextResource.read(cache, [text, ms]);
505+
ReactTestRenderer.unstable_yield(text);
506+
return text;
507+
} catch (promise) {
508+
if (typeof promise.then === 'function') {
509+
ReactTestRenderer.unstable_yield(`Suspend! [${text}]`);
510+
} else {
511+
ReactTestRenderer.unstable_yield(`Error! [${text}]`);
512+
}
513+
throw promise;
514+
}
515+
}
516+
}
517+
518+
function App({text}) {
519+
return (
520+
<Suspense fallback={<Text text="Loading..." />}>
521+
<AsyncTextWithUnmount text={text} ms={100} />
522+
</Suspense>
523+
);
524+
}
525+
526+
const root = ReactTestRenderer.create(<App text="A" />);
527+
expect(ReactTestRenderer).toHaveYielded(['Suspend! [A]', 'Loading...']);
528+
root.update(<Text text="B" />);
529+
// Should not fire componentWillUnmount
530+
expect(ReactTestRenderer).toHaveYielded(['B']);
531+
expect(root).toMatchRenderedOutput('B');
532+
});
533+
495534
it('retries when an update is scheduled on a timed out tree', () => {
496535
let instance;
497536
class Stateful extends React.Component {

packages/react-test-renderer/src/ReactTestRenderer.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
Profiler,
3030
MemoComponent,
3131
SimpleMemoComponent,
32+
IncompleteClassComponent,
3233
} from 'shared/ReactWorkTags';
3334
import invariant from 'shared/invariant';
3435
import ReactVersion from 'shared/ReactVersion';
@@ -193,6 +194,7 @@ function toTree(node: ?Fiber) {
193194
case Profiler:
194195
case ForwardRef:
195196
case MemoComponent:
197+
case IncompleteClassComponent:
196198
return childrenToTree(node.child);
197199
default:
198200
invariant(

packages/shared/ReactWorkTags.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,4 @@ export const SuspenseComponent = 13;
4545
export const MemoComponent = 14;
4646
export const SimpleMemoComponent = 15;
4747
export const LazyComponent = 16;
48+
export const IncompleteClassComponent = 17;

0 commit comments

Comments
 (0)