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