@samitouri / QOS-React-2 / commits / caae627cd5

Fix switching between dangerouslySetInnerHTML and children

With this, ReactMultiChild handles all of the children-related operations for ReactDOMComponent so that we don't process operations out of order. This is necessary because ReactMultiChild does its own batching so there's no way without its cooperation to get the timing right here. Ideally we'll factor this logic out a bit better in subsequent updates but this is the simplest way to fix #1232 which has embarrassingly been open for over a year.

Ben Alpert committed Jul 19, 2015 at 08:19 UTC caae627cd557812d28d11237b34bff6c661ea8bc
8 files changed +115 -29
src/renderers/dom/client/ReactDOMIDOperations.js
-14
@@ -19,7 +19,6 @@ var ReactMount = require('ReactMount');
19 var ReactPerf = require('ReactPerf');
20
21 var invariant = require('invariant');
22 -var setInnerHTML = require('setInnerHTML');
22
23 /**
24 * Errors for properties that should not be updated with `updatePropertyByID()`.
@@ -115,18 +114,6 @@ var ReactDOMIDOperations = {
114 CSSPropertyOperations.setValueForStyles(node, styles);
115 },
116
118 - /**
119 - * Updates a DOM node's innerHTML.
120 - *
121 - * @param {string} id ID of the node to update.
122 - * @param {string} html An HTML string.
123 - * @internal
124 - */
125 - updateInnerHTMLByID: function(id, html) {
126 - var node = ReactMount.getNode(id);
127 - setInnerHTML(node, html);
128 - },
129 -
117 /**
118 * Updates a DOM node's text content set by `props.content`.
119 *
@@ -171,7 +158,6 @@ ReactPerf.measureMethods(ReactDOMIDOperations, 'ReactDOMIDOperations', {
158 updatePropertyByID: 'updatePropertyByID',
159 deletePropertyByID: 'deletePropertyByID',
160 updateStylesByID: 'updateStylesByID',
174 - updateInnerHTMLByID: 'updateInnerHTMLByID',
161 updateTextContentByID: 'updateTextContentByID',
162 dangerouslyReplaceNodeWithMarkupByID: 'dangerouslyReplaceNodeWithMarkupByID',
163 dangerouslyProcessChildrenUpdates: 'dangerouslyProcessChildrenUpdates',
src/renderers/dom/client/__tests__/ReactDOMIDOperations-test.js
+12 -3
@@ -15,6 +15,7 @@ describe('ReactDOMIDOperations', function() {
15 var DOMPropertyOperations = require('DOMPropertyOperations');
16 var ReactDOMIDOperations = require('ReactDOMIDOperations');
17 var ReactMount = require('ReactMount');
18 + var ReactMultiChildUpdateTypes = require('ReactMultiChildUpdateTypes');
19 var keyOf = require('keyOf');
20
21 it('should disallow updating special properties', function() {
@@ -44,9 +45,17 @@ describe('ReactDOMIDOperations', function() {
45
46 var html = '\n \t <span> \n testContent \t </span> \n \t';
47
47 - ReactDOMIDOperations.updateInnerHTMLByID(
48 - 'testID',
49 - html
48 + ReactDOMIDOperations.dangerouslyProcessChildrenUpdates(
49 + [{
50 + parentID: 'testID',
51 + parentNode: null,
52 + type: ReactMultiChildUpdateTypes.SET_MARKUP,
53 + markupIndex: null,
54 + content: html,
55 + fromIndex: null,
56 + toIndex: null,
57 + }],
58 + []
59 );
60
61 expect(
src/renderers/dom/client/utils/DOMChildrenOperations.js
+8 -1
@@ -15,6 +15,7 @@
15 var Danger = require('Danger');
16 var ReactMultiChildUpdateTypes = require('ReactMultiChildUpdateTypes');
17
18 +var setInnerHTML = require('setInnerHTML');
19 var setTextContent = require('setTextContent');
20 var invariant = require('invariant');
21
@@ -123,10 +124,16 @@ var DOMChildrenOperations = {
124 update.toIndex
125 );
126 break;
127 + case ReactMultiChildUpdateTypes.SET_MARKUP:
128 + setInnerHTML(
129 + update.parentNode,
130 + update.content
131 + );
132 + break;
133 case ReactMultiChildUpdateTypes.TEXT_CONTENT:
134 setTextContent(
135 update.parentNode,
129 - update.textContent
136 + update.content
137 );
138 break;
139 case ReactMultiChildUpdateTypes.REMOVE_NODE:
src/renderers/dom/shared/ReactDOMComponent.js
+1 -4
@@ -884,10 +884,7 @@ ReactDOMComponent.Mixin = {
884 }
885 } else if (nextHtml != null) {
886 if (lastHtml !== nextHtml) {
887 - BackendIDOperations.updateInnerHTMLByID(
888 - this._rootNodeID,
889 - nextHtml
890 - );
887 + this.updateMarkup('' + nextHtml);
888 }
889 } else if (nextChildren != null) {
890 this.updateChildren(nextChildren, transaction, context);
src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js
+24
@@ -309,6 +309,30 @@ describe('ReactDOMComponent', function() {
309 expect(container.firstChild.innerHTML).toEqual('adieu');
310 });
311
312 + it('should transition from innerHTML to children in nested el', function() {
313 + var container = document.createElement('div');
314 + React.render(
315 + <div><div dangerouslySetInnerHTML={{__html: 'bonjour'}} /></div>,
316 + container
317 + );
318 +
319 + expect(container.textContent).toEqual('bonjour');
320 + React.render(<div><div><span>adieu</span></div></div>, container);
321 + expect(container.textContent).toEqual('adieu');
322 + });
323 +
324 + it('should transition from children to innerHTML in nested el', function() {
325 + var container = document.createElement('div');
326 + React.render(<div><div><span>adieu</span></div></div>, container);
327 +
328 + expect(container.textContent).toEqual('adieu');
329 + React.render(
330 + <div><div dangerouslySetInnerHTML={{__html: 'bonjour'}} /></div>,
331 + container
332 + );
333 + expect(container.textContent).toEqual('bonjour');
334 + });
335 +
336 it('should not incur unnecessary DOM mutations', function() {
337 var container = document.createElement('div');
338 React.render(<div value="" />, container);
src/renderers/shared/reconciler/ReactMultiChild.js
+68 -6
@@ -53,14 +53,14 @@ var markupQueue = [];
53 * @param {number} toIndex Destination index.
54 * @private
55 */
56 -function enqueueMarkup(parentID, markup, toIndex) {
56 +function enqueueInsertMarkup(parentID, markup, toIndex) {
57 // NOTE: Null values reduce hidden classes.
58 updateQueue.push({
59 parentID: parentID,
60 parentNode: null,
61 type: ReactMultiChildUpdateTypes.INSERT_MARKUP,
62 markupIndex: markupQueue.push(markup) - 1,
63 - textContent: null,
63 + content: null,
64 fromIndex: null,
65 toIndex: toIndex,
66 });
@@ -81,7 +81,7 @@ function enqueueMove(parentID, fromIndex, toIndex) {
81 parentNode: null,
82 type: ReactMultiChildUpdateTypes.MOVE_EXISTING,
83 markupIndex: null,
84 - textContent: null,
84 + content: null,
85 fromIndex: fromIndex,
86 toIndex: toIndex,
87 });
@@ -101,12 +101,32 @@ function enqueueRemove(parentID, fromIndex) {
101 parentNode: null,
102 type: ReactMultiChildUpdateTypes.REMOVE_NODE,
103 markupIndex: null,
104 - textContent: null,
104 + content: null,
105 fromIndex: fromIndex,
106 toIndex: null,
107 });
108 }
109
110 +/**
111 + * Enqueues setting the markup of a node.
112 + *
113 + * @param {string} parentID ID of the parent component.
114 + * @param {string} markup Markup that renders into an element.
115 + * @private
116 + */
117 +function enqueueSetMarkup(parentID, markup) {
118 + // NOTE: Null values reduce hidden classes.
119 + updateQueue.push({
120 + parentID: parentID,
121 + parentNode: null,
122 + type: ReactMultiChildUpdateTypes.SET_MARKUP,
123 + markupIndex: null,
124 + content: markup,
125 + fromIndex: null,
126 + toIndex: null,
127 + });
128 +}
129 +
130 /**
131 * Enqueues setting the text content.
132 *
@@ -121,7 +141,7 @@ function enqueueTextContent(parentID, textContent) {
141 parentNode: null,
142 type: ReactMultiChildUpdateTypes.TEXT_CONTENT,
143 markupIndex: null,
124 - textContent: textContent,
144 + content: textContent,
145 fromIndex: null,
146 toIndex: null,
147 });
@@ -237,6 +257,38 @@ var ReactMultiChild = {
257 }
258 },
259
260 + /**
261 + * Replaces any rendered children with a markup string.
262 + *
263 + * @param {string} nextMarkup String of markup.
264 + * @internal
265 + */
266 + updateMarkup: function(nextMarkup) {
267 + updateDepth++;
268 + var errorThrown = true;
269 + try {
270 + var prevChildren = this._renderedChildren;
271 + // Remove any rendered children.
272 + ReactChildReconciler.unmountChildren(prevChildren);
273 + for (var name in prevChildren) {
274 + if (prevChildren.hasOwnProperty(name)) {
275 + this._unmountChildByName(prevChildren[name], name);
276 + }
277 + }
278 + this.setMarkup(nextMarkup);
279 + errorThrown = false;
280 + } finally {
281 + updateDepth--;
282 + if (!updateDepth) {
283 + if (errorThrown) {
284 + clearQueue();
285 + } else {
286 + processQueue();
287 + }
288 + }
289 + }
290 + },
291 +
292 /**
293 * Updates the rendered children with new children.
294 *
@@ -355,7 +407,7 @@ var ReactMultiChild = {
407 * @protected
408 */
409 createChild: function(child, mountImage) {
358 - enqueueMarkup(this._rootNodeID, mountImage, child._mountIndex);
410 + enqueueInsertMarkup(this._rootNodeID, mountImage, child._mountIndex);
411 },
412
413 /**
@@ -378,6 +430,16 @@ var ReactMultiChild = {
430 enqueueTextContent(this._rootNodeID, textContent);
431 },
432
433 + /**
434 + * Sets this markup string.
435 + *
436 + * @param {string} markup Markup to set.
437 + * @protected
438 + */
439 + setMarkup: function(markup) {
440 + enqueueSetMarkup(this._rootNodeID, markup);
441 + },
442 +
443 /**
444 * Mounts a child with the supplied name.
445 *
src/renderers/shared/reconciler/ReactMultiChildUpdateTypes.js
+1
@@ -25,6 +25,7 @@ var ReactMultiChildUpdateTypes = keyMirror({
25 INSERT_MARKUP: null,
26 MOVE_EXISTING: null,
27 REMOVE_NODE: null,
28 + SET_MARKUP: null,
29 TEXT_CONTENT: null,
30 });
31
src/test/ReactDefaultPerfAnalysis.js
+1 -1
@@ -20,11 +20,11 @@ var DOM_OPERATION_TYPES = {
20 INSERT_MARKUP: 'set innerHTML',
21 MOVE_EXISTING: 'move',
22 REMOVE_NODE: 'remove',
23 + SET_MARKUP: 'set innerHTML',
24 TEXT_CONTENT: 'set textContent',
25 'updatePropertyByID': 'update attribute',
26 'deletePropertyByID': 'delete attribute',
27 'updateStylesByID': 'update styles',
27 - 'updateInnerHTMLByID': 'set innerHTML',
28 'dangerouslyReplaceNodeWithMarkupByID': 'replace',
29 };
30