diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 3ad78e2a7926..f08189c3ef01 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -856,7 +856,7 @@ function updateClassComponent( ); if (__DEV__) { const inst = workInProgress.stateNode; - if (inst.props !== nextProps) { + if (shouldUpdate && inst.props !== nextProps) { if (!didWarnAboutReassigningProps) { console.error( 'It looks like %s is reassigning its own `this.props` while rendering. ' + diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.js b/packages/react-reconciler/src/ReactFiberClassComponent.js index 7b9be3544e71..a688b891fa51 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.js @@ -997,11 +997,13 @@ function updateClassInstance( cloneUpdateQueue(current, workInProgress); - const oldProps = workInProgress.memoizedProps; - instance.props = + const unresolvedOldProps = workInProgress.memoizedProps; + const oldProps = workInProgress.type === workInProgress.elementType - ? oldProps - : resolveDefaultProps(workInProgress.type, oldProps); + ? unresolvedOldProps + : resolveDefaultProps(workInProgress.type, unresolvedOldProps); + instance.props = oldProps; + const unresolvedNewProps = workInProgress.pendingProps; const oldContext = instance.context; const contextType = ctor.contextType; @@ -1029,7 +1031,10 @@ function updateClassInstance( (typeof instance.UNSAFE_componentWillReceiveProps === 'function' || typeof instance.componentWillReceiveProps === 'function') ) { - if (oldProps !== newProps || oldContext !== nextContext) { + if ( + unresolvedOldProps !== unresolvedNewProps || + oldContext !== nextContext + ) { callComponentWillReceiveProps( workInProgress, instance, @@ -1047,7 +1052,7 @@ function updateClassInstance( newState = workInProgress.memoizedState; if ( - oldProps === newProps && + unresolvedOldProps === unresolvedNewProps && oldState === newState && !hasContextChanged() && !checkHasForceUpdateAfterProcessing() @@ -1056,7 +1061,7 @@ function updateClassInstance( // effect even though we're bailing out, so that cWU/cDU are called. if (typeof instance.componentDidUpdate === 'function') { if ( - oldProps !== current.memoizedProps || + unresolvedOldProps !== current.memoizedProps || oldState !== current.memoizedState ) { workInProgress.effectTag |= Update; @@ -1064,7 +1069,7 @@ function updateClassInstance( } if (typeof instance.getSnapshotBeforeUpdate === 'function') { if ( - oldProps !== current.memoizedProps || + unresolvedOldProps !== current.memoizedProps || oldState !== current.memoizedState ) { workInProgress.effectTag |= Snapshot; @@ -1121,7 +1126,7 @@ function updateClassInstance( // effect even though we're bailing out, so that cWU/cDU are called. if (typeof instance.componentDidUpdate === 'function') { if ( - oldProps !== current.memoizedProps || + unresolvedOldProps !== current.memoizedProps || oldState !== current.memoizedState ) { workInProgress.effectTag |= Update; @@ -1129,7 +1134,7 @@ function updateClassInstance( } if (typeof instance.getSnapshotBeforeUpdate === 'function') { if ( - oldProps !== current.memoizedProps || + unresolvedOldProps !== current.memoizedProps || oldState !== current.memoizedState ) { workInProgress.effectTag |= Snapshot; diff --git a/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js index d542173fce30..33533a754586 100644 --- a/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js @@ -343,6 +343,97 @@ describe('ReactLazy', () => { expect(root).toMatchRenderedOutput('SiblingB'); }); + it('resolves defaultProps without breaking bailout due to unchanged props and state, #17151', async () => { + class LazyImpl extends React.Component { + static defaultProps = {value: 0}; + + render() { + const text = `${this.props.label}: ${this.props.value}`; + return ; + } + } + + const Lazy = lazy(() => fakeImport(LazyImpl)); + + const instance1 = React.createRef(null); + const instance2 = React.createRef(null); + + const root = ReactTestRenderer.create( + <> + + }> + + + , + { + unstable_isConcurrent: true, + }, + ); + expect(Scheduler).toFlushAndYield(['Not lazy: 0', 'Loading...']); + expect(root).not.toMatchRenderedOutput('Not lazy: 0Lazy: 0'); + + await Promise.resolve(); + + expect(Scheduler).toFlushAndYield(['Lazy: 0']); + expect(root).toMatchRenderedOutput('Not lazy: 0Lazy: 0'); + + // Should bailout due to unchanged props and state + instance1.current.setState(null); + expect(Scheduler).toFlushAndYield([]); + expect(root).toMatchRenderedOutput('Not lazy: 0Lazy: 0'); + + // Should bailout due to unchanged props and state + instance2.current.setState(null); + expect(Scheduler).toFlushAndYield([]); + expect(root).toMatchRenderedOutput('Not lazy: 0Lazy: 0'); + }); + + it('resolves defaultProps without breaking bailout in PureComponent, #17151', async () => { + class LazyImpl extends React.PureComponent { + static defaultProps = {value: 0}; + state = {}; + + render() { + const text = `${this.props.label}: ${this.props.value}`; + return ; + } + } + + const Lazy = lazy(() => fakeImport(LazyImpl)); + + const instance1 = React.createRef(null); + const instance2 = React.createRef(null); + + const root = ReactTestRenderer.create( + <> + + }> + + + , + { + unstable_isConcurrent: true, + }, + ); + expect(Scheduler).toFlushAndYield(['Not lazy: 0', 'Loading...']); + expect(root).not.toMatchRenderedOutput('Not lazy: 0Lazy: 0'); + + await Promise.resolve(); + + expect(Scheduler).toFlushAndYield(['Lazy: 0']); + expect(root).toMatchRenderedOutput('Not lazy: 0Lazy: 0'); + + // Should bailout due to shallow equal props and state + instance1.current.setState({}); + expect(Scheduler).toFlushAndYield([]); + expect(root).toMatchRenderedOutput('Not lazy: 0Lazy: 0'); + + // Should bailout due to shallow equal props and state + instance2.current.setState({}); + expect(Scheduler).toFlushAndYield([]); + expect(root).toMatchRenderedOutput('Not lazy: 0Lazy: 0'); + }); + it('sets defaultProps for modern lifecycles', async () => { class C extends React.Component { static defaultProps = {text: 'A'};