Updated ReactCompositeComponent to trigger componentWillReceiveProps when either props or context change. Fixes issue #5756
Miles Johnson committed
Jan 5, 2016 at 18:14 UTC
f4c5b2c00b6b59c0db8efbf7419c8549d7cef7eb
2 files changed
+134
-10
src/renderers/shared/reconciler/ReactCompositeComponent.js
+17
-10
@@ -647,12 +647,18 @@ var ReactCompositeComponentMixin = {
647
nextUnmaskedContext
648
) {
649
var inst = this._instance;
650
-
651
- var nextContext = this._context === nextUnmaskedContext ?
652
- inst.context :
653
- this._processContext(nextUnmaskedContext);
650
+ var willReceive = false;
651
+ var nextContext;
652
var nextProps;
653
654
+ // Determine if the context has changed or not
655
+ if (this._context === nextUnmaskedContext) {
656
+ nextContext = inst.context;
657
+ } else {
658
+ nextContext = this._processContext(nextUnmaskedContext);
659
+ willReceive = true;
660
+ }
661
+
662
// Distinguish between a props update versus a simple state update
663
if (prevParentElement === nextParentElement) {
664
// Skip checking prop types again -- we don't read inst.props to avoid
@@ -660,13 +666,14 @@ var ReactCompositeComponentMixin = {
666
nextProps = nextParentElement.props;
667
} else {
668
nextProps = this._processProps(nextParentElement.props);
663
- // An update here will schedule an update but immediately set
664
- // _pendingStateQueue which will ensure that any state updates gets
665
- // immediately reconciled instead of waiting for the next batch.
669
+ willReceive = true;
670
+ }
671
667
- if (inst.componentWillReceiveProps) {
668
- inst.componentWillReceiveProps(nextProps, nextContext);
669
- }
672
+ // An update here will schedule an update but immediately set
673
+ // _pendingStateQueue which will ensure that any state updates gets
674
+ // immediately reconciled instead of waiting for the next batch.
675
+ if (willReceive && inst.componentWillReceiveProps) {
676
+ inst.componentWillReceiveProps(nextProps, nextContext);
677
}
678
679
var nextState = this._processPendingState(nextProps, nextContext);
src/renderers/shared/reconciler/__tests__/ReactCompositeComponent-test.js
+117
@@ -874,6 +874,123 @@ describe('ReactCompositeComponent', function() {
874
expect(div.children[0].id).toBe('aliens');
875
});
876
877
+ it('should trigger componentWillReceiveProps for context changes', function() {
878
+ var contextChanges = 0;
879
+ var propChanges = 0;
880
+
881
+ var GrandChild = React.createClass({
882
+ contextTypes: {
883
+ foo: ReactPropTypes.string.isRequired,
884
+ },
885
+
886
+ componentWillReceiveProps: function(nextProps, nextContext) {
887
+ expect('foo' in nextContext).toBe(true);
888
+
889
+ if (nextProps !== this.props) {
890
+ propChanges++;
891
+ }
892
+
893
+ if (nextContext !== this.context) {
894
+ contextChanges++;
895
+ }
896
+ },
897
+
898
+ render: function() {
899
+ return <span className="grand-child">{this.props.children}</span>;
900
+ },
901
+ });
902
+
903
+ var ChildWithContext = React.createClass({
904
+ contextTypes: {
905
+ foo: ReactPropTypes.string.isRequired,
906
+ },
907
+
908
+ componentWillReceiveProps: function(nextProps, nextContext) {
909
+ expect('foo' in nextContext).toBe(true);
910
+
911
+ if (nextProps !== this.props) {
912
+ propChanges++;
913
+ }
914
+
915
+ if (nextContext !== this.context) {
916
+ contextChanges++;
917
+ }
918
+ },
919
+
920
+ render: function() {
921
+ return <div className="child-with">{this.props.children}</div>;
922
+ },
923
+ });
924
+
925
+ var ChildWithoutContext = React.createClass({
926
+ componentWillReceiveProps: function(nextProps, nextContext) {
927
+ expect('foo' in nextContext).toBe(false);
928
+
929
+ if (nextProps !== this.props) {
930
+ propChanges++;
931
+ }
932
+
933
+ if (nextContext !== this.context) {
934
+ contextChanges++;
935
+ }
936
+ },
937
+
938
+ render: function() {
939
+ return <div className="child-without">{this.props.children}</div>;
940
+ },
941
+ });
942
+
943
+ var Parent = React.createClass({
944
+ childContextTypes: {
945
+ foo: ReactPropTypes.string,
946
+ },
947
+
948
+ getInitialState() {
949
+ return {
950
+ foo: 'abc',
951
+ };
952
+ },
953
+
954
+ getChildContext: function() {
955
+ return {
956
+ foo: this.state.foo,
957
+ };
958
+ },
959
+
960
+ onClick() {
961
+ this.setState({
962
+ foo: 'def',
963
+ });
964
+ },
965
+
966
+ render: function() {
967
+ return <div className="parent" onClick={this.onClick}>{this.props.children}</div>;
968
+ },
969
+ });
970
+
971
+ var div = document.createElement('div');
972
+
973
+ ReactDOM.render(
974
+ <Parent>
975
+ <ChildWithoutContext>
976
+ A1
977
+ <GrandChild>A2</GrandChild>
978
+ </ChildWithoutContext>
979
+
980
+ <ChildWithContext>
981
+ B1
982
+ <GrandChild>B2</GrandChild>
983
+ </ChildWithContext>
984
+ </Parent>,
985
+ div
986
+ );
987
+
988
+ ReactTestUtils.Simulate.click(div.childNodes[0]);
989
+
990
+ expect(propChanges).toBe(0);
991
+ expect(contextChanges).toBe(3); // ChildWithContext, GrandChild x 2
992
+ });
993
+
994
it('should disallow nested render calls', function() {
995
var Inner = React.createClass({
996
render: function() {