Skip to content

Commit 241103a

Browse files
authored
Fix bailout broken in lazy components due to default props resolving (#18539)
* Add failing tests for lazy components * Fix bailout broken in lazy components due to default props resolving We should never compare unresolved props with resolved props. Since comparing resolved props by reference doesn't make sense, we use unresolved props in that case. Otherwise, resolved props are used. * Avoid reassigning props warning when we bailout
1 parent 2dddd1e commit 241103a

File tree

3 files changed

+107
-11
lines changed

3 files changed

+107
-11
lines changed

packages/react-reconciler/src/ReactFiberBeginWork.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -856,7 +856,7 @@ function updateClassComponent(
856856
);
857857
if (__DEV__) {
858858
const inst = workInProgress.stateNode;
859-
if (inst.props !== nextProps) {
859+
if (shouldUpdate && inst.props !== nextProps) {
860860
if (!didWarnAboutReassigningProps) {
861861
console.error(
862862
'It looks like %s is reassigning its own `this.props` while rendering. ' +

packages/react-reconciler/src/ReactFiberClassComponent.js

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -997,11 +997,13 @@ function updateClassInstance(
997997

998998
cloneUpdateQueue(current, workInProgress);
999999

1000-
const oldProps = workInProgress.memoizedProps;
1001-
instance.props =
1000+
const unresolvedOldProps = workInProgress.memoizedProps;
1001+
const oldProps =
10021002
workInProgress.type === workInProgress.elementType
1003-
? oldProps
1004-
: resolveDefaultProps(workInProgress.type, oldProps);
1003+
? unresolvedOldProps
1004+
: resolveDefaultProps(workInProgress.type, unresolvedOldProps);
1005+
instance.props = oldProps;
1006+
const unresolvedNewProps = workInProgress.pendingProps;
10051007

10061008
const oldContext = instance.context;
10071009
const contextType = ctor.contextType;
@@ -1029,7 +1031,10 @@ function updateClassInstance(
10291031
(typeof instance.UNSAFE_componentWillReceiveProps === 'function' ||
10301032
typeof instance.componentWillReceiveProps === 'function')
10311033
) {
1032-
if (oldProps !== newProps || oldContext !== nextContext) {
1034+
if (
1035+
unresolvedOldProps !== unresolvedNewProps ||
1036+
oldContext !== nextContext
1037+
) {
10331038
callComponentWillReceiveProps(
10341039
workInProgress,
10351040
instance,
@@ -1047,7 +1052,7 @@ function updateClassInstance(
10471052
newState = workInProgress.memoizedState;
10481053

10491054
if (
1050-
oldProps === newProps &&
1055+
unresolvedOldProps === unresolvedNewProps &&
10511056
oldState === newState &&
10521057
!hasContextChanged() &&
10531058
!checkHasForceUpdateAfterProcessing()
@@ -1056,15 +1061,15 @@ function updateClassInstance(
10561061
// effect even though we're bailing out, so that cWU/cDU are called.
10571062
if (typeof instance.componentDidUpdate === 'function') {
10581063
if (
1059-
oldProps !== current.memoizedProps ||
1064+
unresolvedOldProps !== current.memoizedProps ||
10601065
oldState !== current.memoizedState
10611066
) {
10621067
workInProgress.effectTag |= Update;
10631068
}
10641069
}
10651070
if (typeof instance.getSnapshotBeforeUpdate === 'function') {
10661071
if (
1067-
oldProps !== current.memoizedProps ||
1072+
unresolvedOldProps !== current.memoizedProps ||
10681073
oldState !== current.memoizedState
10691074
) {
10701075
workInProgress.effectTag |= Snapshot;
@@ -1121,15 +1126,15 @@ function updateClassInstance(
11211126
// effect even though we're bailing out, so that cWU/cDU are called.
11221127
if (typeof instance.componentDidUpdate === 'function') {
11231128
if (
1124-
oldProps !== current.memoizedProps ||
1129+
unresolvedOldProps !== current.memoizedProps ||
11251130
oldState !== current.memoizedState
11261131
) {
11271132
workInProgress.effectTag |= Update;
11281133
}
11291134
}
11301135
if (typeof instance.getSnapshotBeforeUpdate === 'function') {
11311136
if (
1132-
oldProps !== current.memoizedProps ||
1137+
unresolvedOldProps !== current.memoizedProps ||
11331138
oldState !== current.memoizedState
11341139
) {
11351140
workInProgress.effectTag |= Snapshot;

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

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,97 @@ describe('ReactLazy', () => {
343343
expect(root).toMatchRenderedOutput('SiblingB');
344344
});
345345

346+
it('resolves defaultProps without breaking bailout due to unchanged props and state, #17151', async () => {
347+
class LazyImpl extends React.Component {
348+
static defaultProps = {value: 0};
349+
350+
render() {
351+
const text = `${this.props.label}: ${this.props.value}`;
352+
return <Text text={text} />;
353+
}
354+
}
355+
356+
const Lazy = lazy(() => fakeImport(LazyImpl));
357+
358+
const instance1 = React.createRef(null);
359+
const instance2 = React.createRef(null);
360+
361+
const root = ReactTestRenderer.create(
362+
<>
363+
<LazyImpl ref={instance1} label="Not lazy" />
364+
<Suspense fallback={<Text text="Loading..." />}>
365+
<Lazy ref={instance2} label="Lazy" />
366+
</Suspense>
367+
</>,
368+
{
369+
unstable_isConcurrent: true,
370+
},
371+
);
372+
expect(Scheduler).toFlushAndYield(['Not lazy: 0', 'Loading...']);
373+
expect(root).not.toMatchRenderedOutput('Not lazy: 0Lazy: 0');
374+
375+
await Promise.resolve();
376+
377+
expect(Scheduler).toFlushAndYield(['Lazy: 0']);
378+
expect(root).toMatchRenderedOutput('Not lazy: 0Lazy: 0');
379+
380+
// Should bailout due to unchanged props and state
381+
instance1.current.setState(null);
382+
expect(Scheduler).toFlushAndYield([]);
383+
expect(root).toMatchRenderedOutput('Not lazy: 0Lazy: 0');
384+
385+
// Should bailout due to unchanged props and state
386+
instance2.current.setState(null);
387+
expect(Scheduler).toFlushAndYield([]);
388+
expect(root).toMatchRenderedOutput('Not lazy: 0Lazy: 0');
389+
});
390+
391+
it('resolves defaultProps without breaking bailout in PureComponent, #17151', async () => {
392+
class LazyImpl extends React.PureComponent {
393+
static defaultProps = {value: 0};
394+
state = {};
395+
396+
render() {
397+
const text = `${this.props.label}: ${this.props.value}`;
398+
return <Text text={text} />;
399+
}
400+
}
401+
402+
const Lazy = lazy(() => fakeImport(LazyImpl));
403+
404+
const instance1 = React.createRef(null);
405+
const instance2 = React.createRef(null);
406+
407+
const root = ReactTestRenderer.create(
408+
<>
409+
<LazyImpl ref={instance1} label="Not lazy" />
410+
<Suspense fallback={<Text text="Loading..." />}>
411+
<Lazy ref={instance2} label="Lazy" />
412+
</Suspense>
413+
</>,
414+
{
415+
unstable_isConcurrent: true,
416+
},
417+
);
418+
expect(Scheduler).toFlushAndYield(['Not lazy: 0', 'Loading...']);
419+
expect(root).not.toMatchRenderedOutput('Not lazy: 0Lazy: 0');
420+
421+
await Promise.resolve();
422+
423+
expect(Scheduler).toFlushAndYield(['Lazy: 0']);
424+
expect(root).toMatchRenderedOutput('Not lazy: 0Lazy: 0');
425+
426+
// Should bailout due to shallow equal props and state
427+
instance1.current.setState({});
428+
expect(Scheduler).toFlushAndYield([]);
429+
expect(root).toMatchRenderedOutput('Not lazy: 0Lazy: 0');
430+
431+
// Should bailout due to shallow equal props and state
432+
instance2.current.setState({});
433+
expect(Scheduler).toFlushAndYield([]);
434+
expect(root).toMatchRenderedOutput('Not lazy: 0Lazy: 0');
435+
});
436+
346437
it('sets defaultProps for modern lifecycles', async () => {
347438
class C extends React.Component {
348439
static defaultProps = {text: 'A'};

0 commit comments

Comments
 (0)