Skip to content

Commit 952dcf3

Browse files
acdlitelaurinenas
authored andcommitted
Clear existing text content before inserting children (facebook#8331)
Fixes a bug when updating from a single text child (or dangerouslySetInnerHTML) to regular children, where the previous text content never gets deleted.
1 parent 4016e25 commit 952dcf3

File tree

9 files changed

+96
-18
lines changed

9 files changed

+96
-18
lines changed

scripts/fiber/tests-failing.txt

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,6 @@ src/renderers/dom/shared/__tests__/ReactDOM-test.js
3232
* throws in render() if the update callback is not a function
3333

3434
src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js
35-
* should empty element when removing innerHTML
36-
* should transition from innerHTML to children in nested el
3735
* should warn for children on void elements
3836
* should report component containing invalid styles
3937
* should clean up input value tracking
@@ -104,7 +102,6 @@ src/renderers/shared/shared/__tests__/ReactEmptyComponent-test.js
104102
* throws when rendering null at the top level
105103

106104
src/renderers/shared/shared/__tests__/ReactMultiChildText-test.js
107-
* should correctly handle all possible children for render and update
108105
* should reorder keyed text nodes
109106

110107
src/renderers/shared/shared/__tests__/ReactStatelessComponent-test.js

scripts/fiber/tests-passing.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -601,8 +601,10 @@ src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js
601601
* should update arbitrary attributes for tags containing dashes
602602
* should clear all the styles when removing `style`
603603
* should update styles when `style` changes from null to object
604+
* should empty element when removing innerHTML
604605
* should transition from string content to innerHTML
605606
* should transition from innerHTML to string content
607+
* should transition from innerHTML to children in nested el
606608
* should transition from children to innerHTML in nested el
607609
* should not incur unnecessary DOM mutations for attributes
608610
* should not incur unnecessary DOM mutations for string properties
@@ -1397,6 +1399,7 @@ src/renderers/shared/shared/__tests__/ReactMultiChildReconcile-test.js
13971399
* should insert non-empty children in middle where nulls were
13981400

13991401
src/renderers/shared/shared/__tests__/ReactMultiChildText-test.js
1402+
* should correctly handle all possible children for render and update
14001403
* should throw if rendering both HTML and children
14011404
* should render between nested components and inline children
14021405

src/renderers/dom/fiber/ReactDOMFiber.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,22 @@ var DOMRenderer = ReactFiberReconciler({
117117
updateProperties(domElement, type, oldProps, newProps, root);
118118
},
119119

120+
shouldSetTextContent(props : Props) : boolean {
121+
return (
122+
typeof props.children === 'string' ||
123+
typeof props.children === 'number' ||
124+
(
125+
typeof props.dangerouslySetInnerHTML === 'object' &&
126+
props.dangerouslySetInnerHTML !== null &&
127+
typeof props.dangerouslySetInnerHTML.__html === 'string'
128+
)
129+
);
130+
},
131+
132+
resetTextContent(domElement : Instance) : void {
133+
domElement.textContent = '';
134+
},
135+
120136
createTextInstance(text : string, internalInstanceHandle : Object) : TextInstance {
121137
var textNode : TextInstance = document.createTextNode(text);
122138
precacheFiberNode(internalInstanceHandle, textNode);

src/renderers/noop/ReactNoop.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,15 @@ var NoopRenderer = ReactFiberReconciler({
6868
instance.prop = newProps.prop;
6969
},
7070

71+
shouldSetTextContent(props : Props) : boolean {
72+
return (
73+
typeof props.children === 'string' ||
74+
typeof props.children === 'number'
75+
);
76+
},
77+
78+
resetTextContent(instance : Instance) : void {},
79+
7180
createTextInstance(text : string) : TextInstance {
7281
var inst = { text : text, id: instanceCounter++ };
7382
// Hide from unit tests

src/renderers/shared/fiber/ReactFiberBeginWork.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ var {
5353
} = require('ReactPriorityLevel');
5454
var {
5555
Placement,
56+
ContentReset,
5657
} = require('ReactTypeOfSideEffect');
5758
var ReactCurrentOwner = require('ReactCurrentOwner');
5859
var ReactFiberClassComponent = require('ReactFiberClassComponent');
@@ -210,14 +211,27 @@ module.exports = function<T, P, I, TI, C>(
210211
}
211212

212213
function updateHostComponent(current, workInProgress) {
214+
const nextProps = workInProgress.pendingProps;
215+
const prevProps = current ? current.memoizedProps : null;
213216
let nextChildren = workInProgress.pendingProps.children;
214-
if (typeof nextChildren === 'string' || typeof nextChildren === 'number') {
217+
const isDirectTextChild = config.shouldSetTextContent(nextProps);
218+
if (isDirectTextChild) {
215219
// We special case a direct text child of a host node. This is a common
216220
// case. We won't handle it as a reified child. We will instead handle
217221
// this in the host environment that also have access to this prop. That
218222
// avoids allocating another HostText fiber and traversing it.
219223
nextChildren = null;
224+
} else if (prevProps && (
225+
config.shouldSetTextContent(prevProps) ||
226+
prevProps.children === null ||
227+
typeof prevProps.children === 'undefined' ||
228+
typeof prevProps.children === 'boolean'
229+
)) {
230+
// If we're switching from a direct text child to a normal child, or to
231+
// empty, we need to schedule the text content to be reset.
232+
workInProgress.effectTag |= ContentReset;
220233
}
234+
221235
if (workInProgress.pendingProps.hidden &&
222236
workInProgress.pendingWorkPriority !== OffscreenPriority) {
223237
// If this host component is hidden, we can bail out on the children.

src/renderers/shared/fiber/ReactFiberCommitWork.js

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ var {
3030
Placement,
3131
Update,
3232
Callback,
33+
ContentReset,
3334
} = require('ReactTypeOfSideEffect');
3435

3536
module.exports = function<T, P, I, TI, C>(
@@ -38,6 +39,7 @@ module.exports = function<T, P, I, TI, C>(
3839
) {
3940

4041
const commitUpdate = config.commitUpdate;
42+
const resetTextContent = config.resetTextContent;
4143
const commitTextUpdate = config.commitTextUpdate;
4244

4345
const appendChild = config.appendChild;
@@ -83,6 +85,17 @@ module.exports = function<T, P, I, TI, C>(
8385
throw new Error('Expected to find a host parent.');
8486
}
8587

88+
function getHostParentFiber(fiber : Fiber) : Fiber {
89+
let parent = fiber.return;
90+
while (parent) {
91+
if (isHostParent(parent)) {
92+
return parent;
93+
}
94+
parent = parent.return;
95+
}
96+
throw new Error('Expected to find a host parent.');
97+
}
98+
8699
function isHostParent(fiber : Fiber) : boolean {
87100
return (
88101
fiber.tag === HostComponent ||
@@ -133,12 +146,29 @@ module.exports = function<T, P, I, TI, C>(
133146
}
134147

135148
function commitPlacement(finishedWork : Fiber) : void {
136-
// Clear effect from effect tag before any errors can be thrown, so that
137-
// we don't attempt to do this again
138-
finishedWork.effectTag &= ~Placement;
139-
140149
// Recursively insert all host nodes into the parent.
141-
const parent = getHostParent(finishedWork);
150+
const parentFiber = getHostParentFiber(finishedWork);
151+
let parent;
152+
switch (parentFiber.tag) {
153+
case HostComponent:
154+
parent = parentFiber.stateNode;
155+
break;
156+
case HostContainer:
157+
parent = parentFiber.stateNode.containerInfo;
158+
break;
159+
case Portal:
160+
parent = parentFiber.stateNode.containerInfo;
161+
break;
162+
default:
163+
throw new Error('Invalid host parent fiber.');
164+
}
165+
if (parentFiber.effectTag & ContentReset) {
166+
// Reset the text content of the parent before doing any insertions
167+
resetTextContent(parent);
168+
// Clear ContentReset from the effect tag
169+
parentFiber.effectTag &= ~ContentReset;
170+
}
171+
142172
const before = getHostSibling(finishedWork);
143173
// We only have the top Fiber that was inserted but we need recurse down its
144174
// children to find all the terminal nodes.

src/renderers/shared/fiber/ReactFiberReconciler.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ export type HostConfig<T, P, I, TI, C> = {
4949
prepareUpdate(instance : I, oldProps : P, newProps : P) : boolean,
5050
commitUpdate(instance : I, oldProps : P, newProps : P, internalInstanceHandle : OpaqueNode) : void,
5151

52+
shouldSetTextContent(props : P) : boolean,
53+
resetTextContent(instance : I) : void,
54+
5255
createTextInstance(text : string, internalInstanceHandle : OpaqueNode) : TI,
5356
commitTextUpdate(textInstance : TI, oldText : string, newText : string) : void,
5457

src/renderers/shared/fiber/ReactFiberScheduler.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ var {
4040
Update,
4141
PlacementAndUpdate,
4242
Deletion,
43+
ContentReset,
4344
Callback,
4445
Err,
4546
} = require('ReactTypeOfSideEffect');
@@ -168,11 +169,15 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
168169
pass1: while (true) {
169170
try {
170171
while (effectfulFiber) {
172+
if (effectfulFiber.effectTag & ContentReset) {
173+
config.resetTextContent(effectfulFiber.stateNode);
174+
}
175+
171176
// The following switch statement is only concerned about placement,
172177
// updates, and deletions. To avoid needing to add a case for every
173178
// possible bitmap value, we remove the secondary effects from the
174179
// effect tag and switch on that value.
175-
let primaryEffectTag = effectfulFiber.effectTag & ~(Callback | Err);
180+
let primaryEffectTag = effectfulFiber.effectTag & ~(Callback | Err | ContentReset);
176181
switch (primaryEffectTag) {
177182
case Placement: {
178183
commitPlacement(effectfulFiber);

src/renderers/shared/fiber/ReactTypeOfSideEffect.js

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,15 @@
1212

1313
'use strict';
1414

15-
export type TypeOfSideEffect = 0 | 1 | 2 | 3 | 4 | 8 | 16;
15+
export type TypeOfSideEffect = 0 | 1 | 2 | 3 | 4 | 8 | 16 | 32;
1616

1717
module.exports = {
18-
NoEffect: 0, // 0b00000
19-
Placement: 1, // 0b00001
20-
Update: 2, // 0b00010
21-
PlacementAndUpdate: 3, // 0b00011
22-
Deletion: 4, // 0b00100
23-
Callback: 8, // 0b01000
24-
Err: 16, // 0b10000
18+
NoEffect: 0, // 0b000000
19+
Placement: 1, // 0b000001
20+
Update: 2, // 0b000010
21+
PlacementAndUpdate: 3, // 0b000011
22+
Deletion: 4, // 0b000100
23+
ContentReset: 8, // 0b001000
24+
Callback: 16, // 0b010000
25+
Err: 32, // 0b100000
2526
};

0 commit comments

Comments
 (0)