Skip to content

Commit 6ba7a1a

Browse files
committed
Clear existing text content before inserting children
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 a8703e6 commit 6ba7a1a

File tree

9 files changed

+78
-17
lines changed

9 files changed

+78
-17
lines changed

scripts/fiber/tests-failing.txt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,6 @@ src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js
7777
* should clear a single style prop when changing `style`
7878
* should update arbitrary attributes for tags containing dashes
7979
* should update styles when `style` changes from null to object
80-
* should empty element when removing innerHTML
81-
* should transition from innerHTML to children in nested el
8280
* should not incur unnecessary DOM mutations for attributes
8381
* should not incur unnecessary DOM mutations for string properties
8482
* should not incur unnecessary DOM mutations for boolean properties

scripts/fiber/tests-passing.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,8 +530,10 @@ src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js
530530
* should skip reserved props on web components
531531
* should skip dangerouslySetInnerHTML on web components
532532
* should clear all the styles when removing `style`
533+
* should empty element when removing innerHTML
533534
* should transition from string content to innerHTML
534535
* should transition from innerHTML to string content
536+
* should transition from innerHTML to children in nested el
535537
* should transition from children to innerHTML in nested el
536538
* handles multiple child updates without interference
537539
* should generate the correct markup with className

src/renderers/dom/fiber/ReactDOMFiber.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,22 @@ var DOMRenderer = ReactFiberReconciler({
8989
return true;
9090
},
9191

92+
shouldSetTextContent(props : Props) : boolean {
93+
return (
94+
typeof props.children === 'string' ||
95+
typeof props.children === 'number' ||
96+
(
97+
typeof props.dangerouslySetInnerHTML === 'object' &&
98+
props.dangerouslySetInnerHTML !== null &&
99+
typeof props.dangerouslySetInnerHTML.__html === 'string'
100+
)
101+
);
102+
},
103+
104+
resetTextContent(domElement : Instance) : void {
105+
domElement.textContent = '';
106+
},
107+
92108
commitUpdate(domElement : Instance, oldProps : Props, newProps : Props) : void {
93109
if (typeof newProps.className !== 'undefined') {
94110
domElement.className = newProps.className;

src/renderers/noop/ReactNoop.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,15 @@ var NoopRenderer = ReactFiberReconciler({
8888
return true;
8989
},
9090

91+
shouldSetTextContent(props : Props) : boolean {
92+
return (
93+
typeof props.children === 'string' ||
94+
typeof props.children === 'number'
95+
);
96+
},
97+
98+
resetTextContent(instance : Instance) : void {},
99+
91100
commitUpdate(instance : Instance, oldProps : Props, newProps : Props) : void {
92101
instance.prop = newProps.prop;
93102
},

src/renderers/shared/fiber/ReactFiberBeginWork.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ var {
5050
} = require('ReactPriorityLevel');
5151
var {
5252
Placement,
53+
ContentReset,
5354
} = require('ReactTypeOfSideEffect');
5455
var ReactCurrentOwner = require('ReactCurrentOwner');
5556
var ReactFiberClassComponent = require('ReactFiberClassComponent');
@@ -207,14 +208,27 @@ module.exports = function<T, P, I, TI, C>(
207208
}
208209

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

src/renderers/shared/fiber/ReactFiberCommitWork.js

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ var {
2929
Placement,
3030
Update,
3131
Callback,
32+
ContentReset,
3233
} = require('ReactTypeOfSideEffect');
3334

3435
module.exports = function<T, P, I, TI, C>(
@@ -37,6 +38,7 @@ module.exports = function<T, P, I, TI, C>(
3738
) {
3839

3940
const updateContainer = config.updateContainer;
41+
const resetTextContent = config.resetTextContent;
4042
const commitUpdate = config.commitUpdate;
4143
const commitTextUpdate = config.commitTextUpdate;
4244

@@ -67,12 +69,12 @@ module.exports = function<T, P, I, TI, C>(
6769
}
6870
}
6971

70-
function getHostParent(fiber : Fiber) : ?I {
72+
function getHostParent(fiber : Fiber) : ?Fiber {
7173
let parent = fiber.return;
7274
while (parent) {
7375
switch (parent.tag) {
7476
case HostComponent:
75-
return parent.stateNode;
77+
return parent;
7678
case HostContainer:
7779
// TODO: Currently we use the updateContainer feature to update these,
7880
// but we should be able to handle this case too.
@@ -124,10 +126,19 @@ module.exports = function<T, P, I, TI, C>(
124126

125127
function commitInsertion(finishedWork : Fiber) : void {
126128
// Recursively insert all host nodes into the parent.
127-
const parent = getHostParent(finishedWork);
128-
if (!parent) {
129+
const parentFiber = getHostParent(finishedWork);
130+
if (!parentFiber) {
129131
return;
130132
}
133+
const parent : I = parentFiber.stateNode;
134+
135+
if (parentFiber.effectTag & ContentReset) {
136+
// Reset the text content of the parent before doing any insertions
137+
resetTextContent(parent);
138+
// Clear ContentReset from the effect tag
139+
parentFiber.effectTag &= ~ContentReset;
140+
}
141+
131142
const before = getHostSibling(finishedWork);
132143
// We only have the top Fiber that was inserted but we need recurse down its
133144
// children to find all the terminal nodes.
@@ -219,7 +230,8 @@ module.exports = function<T, P, I, TI, C>(
219230

220231
function commitDeletion(current : Fiber) : void {
221232
// Recursively delete all host nodes from the parent.
222-
const parent = getHostParent(current);
233+
const parentFiber = getHostParent(current);
234+
const parent = parentFiber ? parentFiber.stateNode : null;
223235
// Detach refs and call componentWillUnmount() on the whole subtree.
224236
unmountHostComponents(parent, current);
225237

src/renderers/shared/fiber/ReactFiberReconciler.js

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

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

src/renderers/shared/fiber/ReactFiberScheduler.js

Lines changed: 7 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');
@@ -160,12 +161,17 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
160161
// First, we'll perform all the host insertions, updates, deletions and
161162
// ref unmounts.
162163
let effectfulFiber = finishedWork.firstEffect;
164+
163165
while (effectfulFiber) {
166+
if (effectfulFiber.effectTag & ContentReset) {
167+
config.resetTextContent(effectfulFiber.stateNode);
168+
}
169+
164170
// The following switch statement is only concerned about placement,
165171
// updates, and deletions. To avoid needing to add a case for every
166172
// possible bitmap value, we remove the secondary effects from the
167173
// effect tag and switch on that value.
168-
let primaryEffectTag = effectfulFiber.effectTag & ~(Callback | Err);
174+
let primaryEffectTag = effectfulFiber.effectTag & ~(Callback | Err | ContentReset);
169175
switch (primaryEffectTag) {
170176
case Placement: {
171177
commitInsertion(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)