Skip to content

Commit caae627

Browse files
committed
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.
1 parent 33a5a64 commit caae627

8 files changed

+115
-29
lines changed

src/renderers/dom/client/ReactDOMIDOperations.js

-14
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ var ReactMount = require('ReactMount');
1919
var ReactPerf = require('ReactPerf');
2020

2121
var invariant = require('invariant');
22-
var setInnerHTML = require('setInnerHTML');
2322

2423
/**
2524
* Errors for properties that should not be updated with `updatePropertyByID()`.
@@ -115,18 +114,6 @@ var ReactDOMIDOperations = {
115114
CSSPropertyOperations.setValueForStyles(node, styles);
116115
},
117116

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-
130117
/**
131118
* Updates a DOM node's text content set by `props.content`.
132119
*
@@ -171,7 +158,6 @@ ReactPerf.measureMethods(ReactDOMIDOperations, 'ReactDOMIDOperations', {
171158
updatePropertyByID: 'updatePropertyByID',
172159
deletePropertyByID: 'deletePropertyByID',
173160
updateStylesByID: 'updateStylesByID',
174-
updateInnerHTMLByID: 'updateInnerHTMLByID',
175161
updateTextContentByID: 'updateTextContentByID',
176162
dangerouslyReplaceNodeWithMarkupByID: 'dangerouslyReplaceNodeWithMarkupByID',
177163
dangerouslyProcessChildrenUpdates: 'dangerouslyProcessChildrenUpdates',

src/renderers/dom/client/__tests__/ReactDOMIDOperations-test.js

+12-3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ describe('ReactDOMIDOperations', function() {
1515
var DOMPropertyOperations = require('DOMPropertyOperations');
1616
var ReactDOMIDOperations = require('ReactDOMIDOperations');
1717
var ReactMount = require('ReactMount');
18+
var ReactMultiChildUpdateTypes = require('ReactMultiChildUpdateTypes');
1819
var keyOf = require('keyOf');
1920

2021
it('should disallow updating special properties', function() {
@@ -44,9 +45,17 @@ describe('ReactDOMIDOperations', function() {
4445

4546
var html = '\n \t <span> \n testContent \t </span> \n \t';
4647

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+
[]
5059
);
5160

5261
expect(

src/renderers/dom/client/utils/DOMChildrenOperations.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
var Danger = require('Danger');
1616
var ReactMultiChildUpdateTypes = require('ReactMultiChildUpdateTypes');
1717

18+
var setInnerHTML = require('setInnerHTML');
1819
var setTextContent = require('setTextContent');
1920
var invariant = require('invariant');
2021

@@ -123,10 +124,16 @@ var DOMChildrenOperations = {
123124
update.toIndex
124125
);
125126
break;
127+
case ReactMultiChildUpdateTypes.SET_MARKUP:
128+
setInnerHTML(
129+
update.parentNode,
130+
update.content
131+
);
132+
break;
126133
case ReactMultiChildUpdateTypes.TEXT_CONTENT:
127134
setTextContent(
128135
update.parentNode,
129-
update.textContent
136+
update.content
130137
);
131138
break;
132139
case ReactMultiChildUpdateTypes.REMOVE_NODE:

src/renderers/dom/shared/ReactDOMComponent.js

+1-4
Original file line numberDiff line numberDiff line change
@@ -884,10 +884,7 @@ ReactDOMComponent.Mixin = {
884884
}
885885
} else if (nextHtml != null) {
886886
if (lastHtml !== nextHtml) {
887-
BackendIDOperations.updateInnerHTMLByID(
888-
this._rootNodeID,
889-
nextHtml
890-
);
887+
this.updateMarkup('' + nextHtml);
891888
}
892889
} else if (nextChildren != null) {
893890
this.updateChildren(nextChildren, transaction, context);

src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js

+24
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,30 @@ describe('ReactDOMComponent', function() {
309309
expect(container.firstChild.innerHTML).toEqual('adieu');
310310
});
311311

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+
312336
it('should not incur unnecessary DOM mutations', function() {
313337
var container = document.createElement('div');
314338
React.render(<div value="" />, container);

src/renderers/shared/reconciler/ReactMultiChild.js

+68-6
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,14 @@ var markupQueue = [];
5353
* @param {number} toIndex Destination index.
5454
* @private
5555
*/
56-
function enqueueMarkup(parentID, markup, toIndex) {
56+
function enqueueInsertMarkup(parentID, markup, toIndex) {
5757
// NOTE: Null values reduce hidden classes.
5858
updateQueue.push({
5959
parentID: parentID,
6060
parentNode: null,
6161
type: ReactMultiChildUpdateTypes.INSERT_MARKUP,
6262
markupIndex: markupQueue.push(markup) - 1,
63-
textContent: null,
63+
content: null,
6464
fromIndex: null,
6565
toIndex: toIndex,
6666
});
@@ -81,7 +81,7 @@ function enqueueMove(parentID, fromIndex, toIndex) {
8181
parentNode: null,
8282
type: ReactMultiChildUpdateTypes.MOVE_EXISTING,
8383
markupIndex: null,
84-
textContent: null,
84+
content: null,
8585
fromIndex: fromIndex,
8686
toIndex: toIndex,
8787
});
@@ -101,12 +101,32 @@ function enqueueRemove(parentID, fromIndex) {
101101
parentNode: null,
102102
type: ReactMultiChildUpdateTypes.REMOVE_NODE,
103103
markupIndex: null,
104-
textContent: null,
104+
content: null,
105105
fromIndex: fromIndex,
106106
toIndex: null,
107107
});
108108
}
109109

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+
110130
/**
111131
* Enqueues setting the text content.
112132
*
@@ -121,7 +141,7 @@ function enqueueTextContent(parentID, textContent) {
121141
parentNode: null,
122142
type: ReactMultiChildUpdateTypes.TEXT_CONTENT,
123143
markupIndex: null,
124-
textContent: textContent,
144+
content: textContent,
125145
fromIndex: null,
126146
toIndex: null,
127147
});
@@ -237,6 +257,38 @@ var ReactMultiChild = {
237257
}
238258
},
239259

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+
240292
/**
241293
* Updates the rendered children with new children.
242294
*
@@ -355,7 +407,7 @@ var ReactMultiChild = {
355407
* @protected
356408
*/
357409
createChild: function(child, mountImage) {
358-
enqueueMarkup(this._rootNodeID, mountImage, child._mountIndex);
410+
enqueueInsertMarkup(this._rootNodeID, mountImage, child._mountIndex);
359411
},
360412

361413
/**
@@ -378,6 +430,16 @@ var ReactMultiChild = {
378430
enqueueTextContent(this._rootNodeID, textContent);
379431
},
380432

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+
381443
/**
382444
* Mounts a child with the supplied name.
383445
*

src/renderers/shared/reconciler/ReactMultiChildUpdateTypes.js

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ var ReactMultiChildUpdateTypes = keyMirror({
2525
INSERT_MARKUP: null,
2626
MOVE_EXISTING: null,
2727
REMOVE_NODE: null,
28+
SET_MARKUP: null,
2829
TEXT_CONTENT: null,
2930
});
3031

src/test/ReactDefaultPerfAnalysis.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@ var DOM_OPERATION_TYPES = {
2020
INSERT_MARKUP: 'set innerHTML',
2121
MOVE_EXISTING: 'move',
2222
REMOVE_NODE: 'remove',
23+
SET_MARKUP: 'set innerHTML',
2324
TEXT_CONTENT: 'set textContent',
2425
'updatePropertyByID': 'update attribute',
2526
'deletePropertyByID': 'delete attribute',
2627
'updateStylesByID': 'update styles',
27-
'updateInnerHTMLByID': 'set innerHTML',
2828
'dangerouslyReplaceNodeWithMarkupByID': 'replace',
2929
};
3030

0 commit comments

Comments
 (0)