@samitouri / QOS-React / commits / 25f9f4563e

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