Skip to content

Commit b71ca93

Browse files
committed
Added getDerivedStateFromProps to ReactFiberClassComponent
Also updated class component and lifecyle tests to cover the added functionality.
1 parent 8d0e001 commit b71ca93

File tree

5 files changed

+340
-33
lines changed

5 files changed

+340
-33
lines changed

packages/react-dom/src/__tests__/ReactComponentLifeCycle-test.js

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,10 @@ describe('ReactComponentLifeCycle', () => {
509509
};
510510
};
511511
class Outer extends React.Component {
512+
static getDerivedStateFromProps(props, prevState) {
513+
log.push('outer getDerivedStateFromProps');
514+
return null;
515+
}
512516
UNSAFE_componentWillMount = logger('outer componentWillMount');
513517
componentDidMount = logger('outer componentDidMount');
514518
UNSAFE_componentWillReceiveProps = logger(
@@ -528,6 +532,10 @@ describe('ReactComponentLifeCycle', () => {
528532
}
529533

530534
class Inner extends React.Component {
535+
static getDerivedStateFromProps(props, prevState) {
536+
log.push('inner getDerivedStateFromProps');
537+
return null;
538+
}
531539
UNSAFE_componentWillMount = logger('inner componentWillMount');
532540
componentDidMount = logger('inner componentDidMount');
533541
UNSAFE_componentWillReceiveProps = logger(
@@ -544,21 +552,33 @@ describe('ReactComponentLifeCycle', () => {
544552

545553
const container = document.createElement('div');
546554
log = [];
547-
ReactDOM.render(<Outer x={17} />, container);
555+
expect(() => ReactDOM.render(<Outer x={1} />, container)).toWarnDev([
556+
'Warning: Outer: Defines both componentWillReceiveProps() and static ' +
557+
'getDerivedStateFromProps() methods. ' +
558+
'We recommend using only getDerivedStateFromProps().',
559+
'Warning: Inner: Defines both componentWillReceiveProps() and static ' +
560+
'getDerivedStateFromProps() methods. ' +
561+
'We recommend using only getDerivedStateFromProps().',
562+
]);
548563
expect(log).toEqual([
564+
'outer getDerivedStateFromProps',
549565
'outer componentWillMount',
566+
'inner getDerivedStateFromProps',
550567
'inner componentWillMount',
551568
'inner componentDidMount',
552569
'outer componentDidMount',
553570
]);
554571

572+
// Dedup warnings
555573
log = [];
556-
ReactDOM.render(<Outer x={42} />, container);
574+
ReactDOM.render(<Outer x={2} />, container);
557575
expect(log).toEqual([
558576
'outer componentWillReceiveProps',
577+
'outer getDerivedStateFromProps',
559578
'outer shouldComponentUpdate',
560579
'outer componentWillUpdate',
561580
'inner componentWillReceiveProps',
581+
'inner getDerivedStateFromProps',
562582
'inner shouldComponentUpdate',
563583
'inner componentWillUpdate',
564584
'inner componentDidUpdate',
@@ -573,6 +593,37 @@ describe('ReactComponentLifeCycle', () => {
573593
]);
574594
});
575595

596+
it('warns about deprecated unsafe lifecycles', function() {
597+
class MyComponent extends React.Component {
598+
componentWillMount() {}
599+
componentWillReceiveProps() {}
600+
componentWillUpdate() {}
601+
render() {
602+
return null;
603+
}
604+
}
605+
606+
const container = document.createElement('div');
607+
expect(() => ReactDOM.render(<MyComponent x={1} />, container)).toWarnDev([
608+
'Warning: MyComponent: componentWillMount() is deprecated and will be ' +
609+
'removed in the next major version. ' +
610+
'Please use UNSAFE_componentWillMount() instead.',
611+
]);
612+
613+
expect(() => ReactDOM.render(<MyComponent x={2} />, container)).toWarnDev([
614+
'Warning: MyComponent: componentWillReceiveProps() is deprecated and ' +
615+
'will be removed in the next major version. ' +
616+
'Please use UNSAFE_componentWillReceiveProps() instead.',
617+
'Warning: MyComponent: componentWillUpdate() is deprecated and will be ' +
618+
'removed in the next major version. ' +
619+
'Please use UNSAFE_componentWillUpdate() instead.',
620+
]);
621+
622+
// Dedupe check (instantiate and update)
623+
ReactDOM.render(<MyComponent key="new" x={1} />, container);
624+
ReactDOM.render(<MyComponent key="new" x={2} />, container);
625+
});
626+
576627
it('calls effects on module-pattern component', function() {
577628
const log = [];
578629

@@ -623,4 +674,22 @@ describe('ReactComponentLifeCycle', () => {
623674
'ref',
624675
]);
625676
});
677+
678+
it('should warn if getDerivedStateFromProps returns undefined', () => {
679+
class MyComponent extends React.Component {
680+
static getDerivedStateFromProps() {}
681+
render() {
682+
return null;
683+
}
684+
}
685+
686+
const div = document.createElement('div');
687+
expect(() => ReactDOM.render(<MyComponent />, div)).toWarnDev(
688+
'MyComponent.getDerivedStateFromProps(): A valid state object (or null) must ' +
689+
'be returned. You may have returned undefined.',
690+
);
691+
692+
// De-duped
693+
ReactDOM.render(<MyComponent />, div);
694+
});
626695
});

packages/react-reconciler/src/ReactFiberClassComponent.js

Lines changed: 112 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,23 @@ import {hasContextChanged} from './ReactFiberContext';
4040
const fakeInternalInstance = {};
4141
const isArray = Array.isArray;
4242

43+
let didWarnAboutLegacyWillMount;
44+
let didWarnAboutLegacyWillReceiveProps;
45+
let didWarnAboutLegacyWillUpdate;
4346
let didWarnAboutStateAssignmentForComponent;
47+
let didWarnAboutUndefinedDerivedState;
48+
let didWarnAboutWillReceivePropsAndDerivedState;
4449
let warnOnInvalidCallback;
4550

4651
if (__DEV__) {
47-
const didWarnOnInvalidCallback = {};
52+
didWarnAboutLegacyWillMount = {};
53+
didWarnAboutLegacyWillReceiveProps = {};
54+
didWarnAboutLegacyWillUpdate = {};
4855
didWarnAboutStateAssignmentForComponent = {};
56+
didWarnAboutUndefinedDerivedState = {};
57+
didWarnAboutWillReceivePropsAndDerivedState = {};
58+
59+
const didWarnOnInvalidCallback = {};
4960

5061
warnOnInvalidCallback = function(callback: mixed, callerName: string) {
5162
if (callback === null || typeof callback === 'function') {
@@ -380,8 +391,17 @@ export default function(
380391
const instance = new ctor(props, context);
381392
adoptClassInstance(workInProgress, instance);
382393

383-
// TODO (getDerivedStateFromProps) Call Component.getDerivedStateFromProps
384-
// Merge the returned value into instance.state
394+
const partialState = callGetDerivedStateFromProps(
395+
workInProgress,
396+
instance,
397+
props,
398+
);
399+
if (partialState) {
400+
// Render-phase updates (like this) should not be added to the update queue,
401+
// So that multiple render passes do not enqueue multiple updates.
402+
// Instead, just synchronously merge the returned state into the instance.
403+
instance.state = Object.assign({}, instance.state, partialState);
404+
}
385405

386406
// Cache unmasked context so we can avoid recreating masked context unless necessary.
387407
// ReactFiberContext usually updates this cache but can't for newly-created instances.
@@ -398,12 +418,16 @@ export default function(
398418

399419
if (typeof instance.componentWillMount === 'function') {
400420
if (__DEV__) {
401-
warning(
402-
false,
403-
'%s: componentWillMount() is deprecated and will be removed in the ' +
404-
'next major version. Please use UNSAFE_componentWillMount() instead.',
405-
getComponentName(workInProgress),
406-
);
421+
const componentName = getComponentName(workInProgress) || 'Unknown';
422+
if (!didWarnAboutLegacyWillMount[componentName]) {
423+
warning(
424+
false,
425+
'%s: componentWillMount() is deprecated and will be removed in the ' +
426+
'next major version. Please use UNSAFE_componentWillMount() instead.',
427+
componentName,
428+
);
429+
didWarnAboutLegacyWillMount[componentName] = true;
430+
}
407431
}
408432
instance.componentWillMount();
409433
} else {
@@ -435,12 +459,16 @@ export default function(
435459
const oldState = instance.state;
436460
if (typeof instance.componentWillReceiveProps === 'function') {
437461
if (__DEV__) {
438-
warning(
439-
false,
440-
'%s: componentWillReceiveProps() is deprecated and will be removed in the ' +
441-
'next major version. Please use UNSAFE_componentWillReceiveProps() instead.',
442-
getComponentName(workInProgress),
443-
);
462+
const componentName = getComponentName(workInProgress) || 'Unknown';
463+
if (!didWarnAboutLegacyWillReceiveProps[componentName]) {
464+
warning(
465+
false,
466+
'%s: componentWillReceiveProps() is deprecated and will be removed in the ' +
467+
'next major version. Please use UNSAFE_componentWillReceiveProps() instead.',
468+
componentName,
469+
);
470+
didWarnAboutLegacyWillReceiveProps[componentName] = true;
471+
}
444472
}
445473

446474
startPhaseTimer(workInProgress, 'componentWillReceiveProps');
@@ -457,18 +485,9 @@ export default function(
457485
}
458486
}
459487

460-
// TODO (getDerivedStateFromProps) If both cWRP and static gDSFP methods exist, warn.
461-
// Call cWRP first then static gDSFP; don't bother trying to sync apply setState() changes.
462-
463-
// TODO (getDerivedStateFromProps) Call Component.getDerivedStateFromProps
464-
465-
// TODO (getDerivedStateFromProps) Returned value should not be added to update queue.
466-
// Just synchronously Object.assign it into instance.state
467-
// This should be covered in a test too.
468-
469488
if (instance.state !== oldState) {
470489
if (__DEV__) {
471-
const componentName = getComponentName(workInProgress) || 'Component';
490+
const componentName = getComponentName(workInProgress) || 'Unknown';
472491
if (!didWarnAboutStateAssignmentForComponent[componentName]) {
473492
warning(
474493
false,
@@ -484,6 +503,50 @@ export default function(
484503
}
485504
}
486505

506+
function callGetDerivedStateFromProps(workInProgress, instance, props) {
507+
const {type} = workInProgress;
508+
509+
if (typeof type.getDerivedStateFromProps === 'function') {
510+
if (__DEV__) {
511+
if (
512+
typeof instance.componentWillReceiveProps === 'function' ||
513+
typeof instance.UNSAFE_componentWillReceiveProps === 'function'
514+
) {
515+
const componentName = getComponentName(workInProgress) || 'Unknown';
516+
if (!didWarnAboutWillReceivePropsAndDerivedState[componentName]) {
517+
warning(
518+
false,
519+
'%s: Defines both componentWillReceiveProps() and static ' +
520+
'getDerivedStateFromProps() methods. We recommend using ' +
521+
'only getDerivedStateFromProps().',
522+
componentName,
523+
);
524+
didWarnAboutWillReceivePropsAndDerivedState[componentName] = true;
525+
}
526+
}
527+
}
528+
529+
const partialState = type.getDerivedStateFromProps(props, instance.state);
530+
531+
if (__DEV__) {
532+
if (partialState === undefined) {
533+
const componentName = getComponentName(workInProgress) || 'Unknown';
534+
if (!didWarnAboutUndefinedDerivedState[componentName]) {
535+
warning(
536+
false,
537+
'%s.getDerivedStateFromProps(): A valid state object (or null) must be returned. ' +
538+
'You may have returned undefined.',
539+
componentName,
540+
);
541+
didWarnAboutUndefinedDerivedState[componentName] = componentName;
542+
}
543+
}
544+
}
545+
546+
return partialState || null;
547+
}
548+
}
549+
487550
// Invokes the mount life-cycles on a previously never rendered instance.
488551
function mountClassInstance(
489552
workInProgress: Fiber,
@@ -675,6 +738,12 @@ export default function(
675738
);
676739
}
677740

741+
const partialState = callGetDerivedStateFromProps(
742+
workInProgress,
743+
instance,
744+
newProps,
745+
);
746+
678747
// Compute the next state using the memoized state and the update queue.
679748
const oldState = workInProgress.memoizedState;
680749
// TODO: Previous state can be null.
@@ -692,6 +761,13 @@ export default function(
692761
newState = oldState;
693762
}
694763

764+
if (partialState) {
765+
// Render-phase updates (like this) should not be added to the update queue,
766+
// So that multiple render passes do not enqueue multiple updates.
767+
// Instead, just synchronously merge the returned state into the instance.
768+
newState = Object.assign({}, newState, partialState);
769+
}
770+
695771
if (
696772
oldProps === newProps &&
697773
oldState === newState &&
@@ -730,12 +806,17 @@ export default function(
730806
) {
731807
if (typeof instance.componentWillUpdate === 'function') {
732808
if (__DEV__) {
733-
warning(
734-
false,
735-
'%s: componentWillUpdate() is deprecated and will be removed in the ' +
736-
'next major version. Please use UNSAFE_componentWillUpdate() instead.',
737-
getComponentName(workInProgress),
738-
);
809+
const componentName =
810+
getComponentName(workInProgress) || 'Component';
811+
if (!didWarnAboutLegacyWillUpdate[componentName]) {
812+
warning(
813+
false,
814+
'%s: componentWillUpdate() is deprecated and will be removed in the ' +
815+
'next major version. Please use UNSAFE_componentWillUpdate() instead.',
816+
componentName,
817+
);
818+
didWarnAboutLegacyWillUpdate[componentName] = true;
819+
}
739820
}
740821

741822
startPhaseTimer(workInProgress, 'componentWillUpdate');

packages/react/src/__tests__/ReactCoffeeScriptClass-test.coffee

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,55 @@ describe 'ReactCoffeeScriptClass', ->
9898
test React.createElement(Foo), 'SPAN', 'bar'
9999
undefined
100100

101+
it 'sets initial state with value returned by static getDerivedStateFromProps', ->
102+
class Foo extends React.Component
103+
render: ->
104+
div
105+
className: "#{@state.foo} #{@state.bar}"
106+
Foo.getDerivedStateFromProps = (nextProps, prevState) ->
107+
{
108+
foo: nextProps.foo
109+
bar: 'bar'
110+
}
111+
test React.createElement(Foo, foo: 'foo'), 'DIV', 'foo bar'
112+
undefined
113+
114+
it 'updates initial state with values returned by static getDerivedStateFromProps', ->
115+
class Foo extends React.Component
116+
constructor: (props, context) ->
117+
super props, context
118+
@state =
119+
foo: 'foo'
120+
bar: 'bar'
121+
render: ->
122+
div
123+
className: "#{@state.foo} #{@state.bar}"
124+
Foo.getDerivedStateFromProps = (nextProps, prevState) ->
125+
{
126+
foo: "not-#{prevState.foo}"
127+
}
128+
test React.createElement(Foo), 'DIV', 'not-foo bar'
129+
undefined
130+
131+
it 'renders updated state with values returned by static getDerivedStateFromProps', ->
132+
class Foo extends React.Component
133+
constructor: (props, context) ->
134+
super props, context
135+
@state =
136+
value: 'initial'
137+
render: ->
138+
div
139+
className: @state.value
140+
Foo.getDerivedStateFromProps = (nextProps, prevState) ->
141+
if nextProps.update
142+
return {
143+
value: 'updated'
144+
}
145+
return null
146+
test React.createElement(Foo, update: false), 'DIV', 'initial'
147+
test React.createElement(Foo, update: true), 'DIV', 'updated'
148+
undefined
149+
101150
it 'renders based on context in the constructor', ->
102151
class Foo extends React.Component
103152
@contextTypes:

0 commit comments

Comments
 (0)