Fix renderSubtreeIntoContainer to update context (#7125)
* create failing test case * Fix renderSubtreeIntoContainer to update context Fixes #6599 * Also test re-rendering due to prop update * Address review feedback
Dan Abramov committed
Jun 27, 2016 at 23:29 UTC
25f9f4563edb0b0daa4687da0c75b3ae010e9816
4 files changed
+123
-11
src/addons/__tests__/renderSubtreeIntoContainer-test.js
+104
-1
@@ -12,13 +12,13 @@
12
'use strict';
13
14
var React = require('React');
15
+var ReactDOM = require('ReactDOM');
16
var ReactTestUtils = require('ReactTestUtils');
17
var renderSubtreeIntoContainer = require('renderSubtreeIntoContainer');
18
19
describe('renderSubtreeIntoContainer', function() {
20
21
it('should pass context when rendering subtree elsewhere', function() {
21
-
22
var portal = document.createElement('div');
23
24
var Component = React.createClass({
@@ -92,4 +92,107 @@ describe('renderSubtreeIntoContainer', function() {
92
},
93
});
94
});
95
+
96
+ it('should update context if it changes due to setState', function() {
97
+ var container = document.createElement('div');
98
+ document.body.appendChild(container);
99
+ var portal = document.createElement('div');
100
+
101
+ var Component = React.createClass({
102
+ contextTypes: {
103
+ foo: React.PropTypes.string.isRequired,
104
+ getFoo: React.PropTypes.func.isRequired,
105
+ },
106
+
107
+ render: function() {
108
+ return <div>{this.context.foo + '-' + this.context.getFoo()}</div>;
109
+ },
110
+ });
111
+
112
+ var Parent = React.createClass({
113
+ childContextTypes: {
114
+ foo: React.PropTypes.string.isRequired,
115
+ getFoo: React.PropTypes.func.isRequired,
116
+ },
117
+
118
+ getChildContext: function() {
119
+ return {
120
+ foo: this.state.bar,
121
+ getFoo: () => this.state.bar,
122
+ };
123
+ },
124
+
125
+ getInitialState: function() {
126
+ return {
127
+ bar: 'initial',
128
+ };
129
+ },
130
+
131
+ render: function() {
132
+ return null;
133
+ },
134
+
135
+ componentDidMount: function() {
136
+ renderSubtreeIntoContainer(this, <Component />, portal);
137
+ },
138
+
139
+ componentDidUpdate() {
140
+ renderSubtreeIntoContainer(this, <Component />, portal);
141
+ },
142
+ });
143
+
144
+ var instance = ReactDOM.render(<Parent />, container);
145
+ expect(portal.firstChild.innerHTML).toBe('initial-initial');
146
+ instance.setState({bar: 'changed'});
147
+ expect(portal.firstChild.innerHTML).toBe('changed-changed');
148
+ });
149
+
150
+ it('should update context if it changes due to re-render', function() {
151
+ var container = document.createElement('div');
152
+ document.body.appendChild(container);
153
+ var portal = document.createElement('div');
154
+
155
+ var Component = React.createClass({
156
+ contextTypes: {
157
+ foo: React.PropTypes.string.isRequired,
158
+ getFoo: React.PropTypes.func.isRequired,
159
+ },
160
+
161
+ render: function() {
162
+ return <div>{this.context.foo + '-' + this.context.getFoo()}</div>;
163
+ },
164
+ });
165
+
166
+ var Parent = React.createClass({
167
+ childContextTypes: {
168
+ foo: React.PropTypes.string.isRequired,
169
+ getFoo: React.PropTypes.func.isRequired,
170
+ },
171
+
172
+ getChildContext: function() {
173
+ return {
174
+ foo: this.props.bar,
175
+ getFoo: () => this.props.bar,
176
+ };
177
+ },
178
+
179
+ render: function() {
180
+ return null;
181
+ },
182
+
183
+ componentDidMount: function() {
184
+ renderSubtreeIntoContainer(this, <Component />, portal);
185
+ },
186
+
187
+ componentDidUpdate() {
188
+ renderSubtreeIntoContainer(this, <Component />, portal);
189
+ },
190
+ });
191
+
192
+ ReactDOM.render(<Parent bar="initial" />, container);
193
+ expect(portal.firstChild.innerHTML).toBe('initial-initial');
194
+ ReactDOM.render(<Parent bar="changed" />, container);
195
+ expect(portal.firstChild.innerHTML).toBe('changed-changed');
196
+ });
197
+
198
});
src/renderers/dom/client/ReactMount.js
+14
-7
@@ -20,6 +20,7 @@ var ReactDOMContainerInfo = require('ReactDOMContainerInfo');
20
var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
21
var ReactElement = require('ReactElement');
22
var ReactFeatureFlags = require('ReactFeatureFlags');
23
+var ReactInstanceMap = require('ReactInstanceMap');
24
var ReactInstrumentation = require('ReactInstrumentation');
25
var ReactMarkupChecksum = require('ReactMarkupChecksum');
26
var ReactReconciler = require('ReactReconciler');
@@ -287,10 +288,11 @@ var ReactMount = {
288
_updateRootComponent: function(
289
prevComponent,
290
nextElement,
291
+ nextContext,
292
container,
293
callback) {
294
ReactMount.scrollMonitor(container, function() {
293
- ReactUpdateQueue.enqueueElementInternal(prevComponent, nextElement);
295
+ ReactUpdateQueue.enqueueElementInternal(prevComponent, nextElement, nextContext);
296
if (callback) {
297
ReactUpdateQueue.enqueueCallbackInternal(prevComponent, callback);
298
}
@@ -389,7 +391,7 @@ var ReactMount = {
391
*/
392
renderSubtreeIntoContainer: function(parentComponent, nextElement, container, callback) {
393
invariant(
392
- parentComponent != null && parentComponent._reactInternalInstance != null,
394
+ parentComponent != null && ReactInstanceMap.has(parentComponent),
395
'parentComponent must be a valid React Component'
396
);
397
return ReactMount._renderSubtreeIntoContainer(
@@ -440,6 +442,14 @@ var ReactMount = {
442
nextElement
443
);
444
445
+ var nextContext;
446
+ if (parentComponent) {
447
+ var parentInst = ReactInstanceMap.get(parentComponent);
448
+ nextContext = parentInst._processChildContext(parentInst._context);
449
+ } else {
450
+ nextContext = emptyObject;
451
+ }
452
+
453
var prevComponent = getTopLevelWrapperInContainer(container);
454
455
if (prevComponent) {
@@ -453,6 +463,7 @@ var ReactMount = {
463
ReactMount._updateRootComponent(
464
prevComponent,
465
nextWrappedElement,
466
+ nextContext,
467
container,
468
updatedCallback
469
);
@@ -501,11 +512,7 @@ var ReactMount = {
512
nextWrappedElement,
513
container,
514
shouldReuseMarkup,
504
- parentComponent != null ?
505
- parentComponent._reactInternalInstance._processChildContext(
506
- parentComponent._reactInternalInstance._context
507
- ) :
508
- emptyObject
515
+ nextContext
516
)._renderedComponent.getPublicInstance();
517
if (callback) {
518
callback.call(component);
src/renderers/native/ReactNativeMount.js
+1
-1
@@ -118,7 +118,7 @@ var ReactNativeMount = {
118
var prevWrappedElement = prevComponent._currentElement;
119
var prevElement = prevWrappedElement.props;
120
if (shouldUpdateReactComponent(prevElement, nextElement)) {
121
- ReactUpdateQueue.enqueueElementInternal(prevComponent, nextWrappedElement);
121
+ ReactUpdateQueue.enqueueElementInternal(prevComponent, nextWrappedElement, emptyObject);
122
if (callback) {
123
ReactUpdateQueue.enqueueCallbackInternal(prevComponent, callback);
124
}
src/renderers/shared/stack/reconciler/ReactUpdateQueue.js
+4
-2
@@ -246,8 +246,10 @@ var ReactUpdateQueue = {
246
enqueueUpdate(internalInstance);
247
},
248
249
- enqueueElementInternal: function(internalInstance, newElement) {
250
- internalInstance._pendingElement = newElement;
249
+ enqueueElementInternal: function(internalInstance, nextElement, nextContext) {
250
+ internalInstance._pendingElement = nextElement;
251
+ // TODO: introduce _pendingContext instead of setting it directly.
252
+ internalInstance._context = nextContext;
253
enqueueUpdate(internalInstance);
254
},
255