Update root children using the appendChild/insertBefore/removeChild methods
This removes updateContainer and instead uses the regular child mutation methods to insert into the root container and portals. Since we're no longer clearing out the container DOM in updateContainer we have to do that manually during initial mount. This now works on a document and one of the tests end up unmounting the body when you render into the document so I had to work around that bit since we don't yet properly support rendering into the document root.
Sebastian Markbage committed
Nov 23, 2016 at 15:30 UTC
78add2dc639e4a8aaa9ea77bec64e45fac6aa11b
8 files changed
+76
-60
scripts/fiber/tests-failing.txt
-1
@@ -120,4 +120,3 @@ src/renderers/shared/stack/reconciler/__tests__/refs-test.js
120
121
src/test/__tests__/ReactTestUtils-test.js
122
* traverses children in the correct order
123
-* should support injected wrapper components as DOM components
scripts/fiber/tests-passing.txt
+1
@@ -1560,6 +1560,7 @@ src/test/__tests__/ReactTestUtils-test.js
1560
* can scryRenderedDOMComponentsWithClass with TextComponent
1561
* can scryRenderedDOMComponentsWithClass with className contains \n
1562
* can scryRenderedDOMComponentsWithClass with multiple classes
1563
+* should support injected wrapper components as DOM components
1564
* should change the value of an input field
1565
* should change the value of an input field in a component
1566
* should throw when attempting to use ReactTestUtils.Simulate with shallow rendering
src/renderers/dom/fiber/ReactDOMFiber.js
+16
-14
@@ -38,6 +38,8 @@ var {
38
} = ReactDOMFiberComponent;
39
var { precacheFiberNode } = ReactDOMComponentTree;
40
41
+const DOCUMENT_NODE = 9;
42
+
43
ReactDOMInjection.inject();
44
ReactControlledComponent.injection.injectFiberControlledHostComponent(
45
ReactDOMFiberComponent
@@ -88,19 +90,13 @@ var DOMRenderer = ReactFiberReconciler({
90
eventsEnabled = null;
91
},
92
91
- updateContainer(container : Container, children : HostChildren<Instance | TextInstance>) : void {
92
- // TODO: Containers should update similarly to other parents.
93
- container.innerHTML = '';
94
- recursivelyAppendChildren(container, children);
95
- },
96
-
93
createInstance(
94
type : string,
95
props : Props,
96
children : HostChildren<Instance | TextInstance>,
97
internalInstanceHandle : Object
98
) : Instance {
103
- const root = document.body; // HACK
99
+ const root = document.documentElement; // HACK
100
101
const domElement : Instance = createElement(type, props, root);
102
precacheFiberNode(internalInstanceHandle, domElement);
@@ -124,7 +120,7 @@ var DOMRenderer = ReactFiberReconciler({
120
internalInstanceHandle : Object
121
) : void {
122
var type = domElement.tagName.toLowerCase(); // HACK
127
- var root = document.body; // HACK
123
+ var root = document.documentElement; // HACK
124
// Update the internal instance handle so that we know which props are
125
// the current ones.
126
precacheFiberNode(internalInstanceHandle, domElement);
@@ -141,19 +137,19 @@ var DOMRenderer = ReactFiberReconciler({
137
textInstance.nodeValue = newText;
138
},
139
144
- appendChild(parentInstance : Instance, child : Instance | TextInstance) : void {
140
+ appendChild(parentInstance : Instance | Container, child : Instance | TextInstance) : void {
141
parentInstance.appendChild(child);
142
},
143
144
insertBefore(
149
- parentInstance : Instance,
145
+ parentInstance : Instance | Container,
146
child : Instance | TextInstance,
147
beforeChild : Instance | TextInstance
148
) : void {
149
parentInstance.insertBefore(child, beforeChild);
150
},
151
156
- removeChild(parentInstance : Instance, child : Instance | TextInstance) : void {
152
+ removeChild(parentInstance : Instance | Container, child : Instance | TextInstance) : void {
153
parentInstance.removeChild(child);
154
},
155
@@ -177,9 +173,15 @@ function warnAboutUnstableUse() {
173
warned = true;
174
}
175
180
-function renderSubtreeIntoContainer(parentComponent : ?ReactComponent<any, any, any>, element : ReactElement<any>, container : DOMContainerElement, callback: ?Function) {
176
+function renderSubtreeIntoContainer(parentComponent : ?ReactComponent<any, any, any>, element : ReactElement<any>, containerNode : DOMContainerElement | Document, callback: ?Function) {
177
+ let container : DOMContainerElement =
178
+ containerNode.nodeType === DOCUMENT_NODE ? (containerNode : any).documentElement : (containerNode : any);
179
let root;
180
if (!container._reactRootContainer) {
181
+ // First clear any existing content.
182
+ while (container.lastChild) {
183
+ container.removeChild(container.lastChild);
184
+ }
185
root = container._reactRootContainer = DOMRenderer.mountContainer(element, container, parentComponent, callback);
186
} else {
187
DOMRenderer.updateContainer(element, root = container._reactRootContainer, parentComponent, callback);
@@ -194,12 +196,12 @@ var ReactDOM = {
196
return renderSubtreeIntoContainer(null, element, container, callback);
197
},
198
197
- unstable_renderSubtreeIntoContainer(parentComponent : ReactComponent<any, any, any>, element : ReactElement<any>, container : DOMContainerElement, callback: ?Function) {
199
+ unstable_renderSubtreeIntoContainer(parentComponent : ReactComponent<any, any, any>, element : ReactElement<any>, containerNode : DOMContainerElement | Document, callback: ?Function) {
200
invariant(
201
parentComponent != null && ReactInstanceMap.has(parentComponent),
202
'parentComponent must be a valid React Component'
203
);
202
- return renderSubtreeIntoContainer(parentComponent, element, container, callback);
204
+ return renderSubtreeIntoContainer(parentComponent, element, containerNode, callback);
205
},
206
207
unmountComponentAtNode(container : DOMContainerElement) {
src/renderers/noop/ReactNoop.js
+3
-7
@@ -67,10 +67,6 @@ function flattenChildren(children : HostChildren<Instance | TextInstance>) {
67
68
var NoopRenderer = ReactFiberReconciler({
69
70
- updateContainer(containerInfo : Container, children : HostChildren<Instance | TextInstance>) : void {
71
- containerInfo.children = flattenChildren(children);
72
- },
73
-
70
createInstance(type : string, props : Props, children : HostChildren<Instance | TextInstance>) : Instance {
71
const inst = {
72
tag: TERMINAL_TAG,
@@ -104,7 +100,7 @@ var NoopRenderer = ReactFiberReconciler({
100
textInstance.text = newText;
101
},
102
107
- appendChild(parentInstance : Instance, child : Instance | TextInstance) : void {
103
+ appendChild(parentInstance : Instance | Container, child : Instance | TextInstance) : void {
104
const index = parentInstance.children.indexOf(child);
105
if (index !== -1) {
106
parentInstance.children.splice(index, 1);
@@ -113,7 +109,7 @@ var NoopRenderer = ReactFiberReconciler({
109
},
110
111
insertBefore(
116
- parentInstance : Instance,
112
+ parentInstance : Instance | Container,
113
child : Instance | TextInstance,
114
beforeChild : Instance | TextInstance
115
) : void {
@@ -128,7 +124,7 @@ var NoopRenderer = ReactFiberReconciler({
124
parentInstance.children.splice(beforeIndex, 0, child);
125
},
126
131
- removeChild(parentInstance : Instance, child : Instance | TextInstance) : void {
127
+ removeChild(parentInstance : Instance | Container, child : Instance | TextInstance) : void {
128
const index = parentInstance.children.indexOf(child);
129
if (index === -1) {
130
throw new Error('This child does not exist.');
src/renderers/shared/fiber/ReactFiberBeginWork.js
+18
-1
@@ -300,7 +300,24 @@ module.exports = function<T, P, I, TI, C>(
300
}
301
302
function updatePortalComponent(current, workInProgress) {
303
- reconcileChildren(current, workInProgress, workInProgress.pendingProps);
303
+ const priorityLevel = workInProgress.pendingWorkPriority;
304
+ const nextChildren = workInProgress.pendingProps;
305
+ if (!current) {
306
+ // Portals are special because we don't append the children during mount
307
+ // but at commit. Therefore we need to track insertions which the normal
308
+ // flow doesn't do during mount. This doesn't happen at the root because
309
+ // the root always starts with a "current" with a null child.
310
+ // TODO: Consider unifying this with how the root works.
311
+ workInProgress.child = reconcileChildFibersInPlace(
312
+ workInProgress,
313
+ workInProgress.child,
314
+ nextChildren,
315
+ priorityLevel
316
+ );
317
+ markChildAsProgressed(current, workInProgress, priorityLevel);
318
+ } else {
319
+ reconcileChildren(current, workInProgress, nextChildren);
320
+ }
321
}
322
323
/*
src/renderers/shared/fiber/ReactFiberCommitWork.js
+32
-23
@@ -13,7 +13,6 @@
13
'use strict';
14
15
import type { Fiber } from 'ReactFiber';
16
-import type { FiberRoot } from 'ReactFiberRoot';
16
import type { HostConfig } from 'ReactFiberReconciler';
17
18
var ReactTypeOfWork = require('ReactTypeOfWork');
@@ -37,7 +36,6 @@ module.exports = function<T, P, I, TI, C>(
36
trapError : (failedFiber : Fiber, error: Error, isUnmounting : boolean) => void
37
) {
38
40
- const updateContainer = config.updateContainer;
39
const commitUpdate = config.commitUpdate;
40
const commitTextUpdate = config.commitTextUpdate;
41
@@ -68,22 +66,30 @@ module.exports = function<T, P, I, TI, C>(
66
}
67
}
68
71
- function getHostParent(fiber : Fiber) : ?I {
69
+ function getHostParent(fiber : Fiber) : null | I | C {
70
let parent = fiber.return;
71
while (parent) {
72
switch (parent.tag) {
73
case HostComponent:
74
return parent.stateNode;
75
case HostContainer:
78
- // TODO: Currently we use the updateContainer feature to update these,
79
- // but we should be able to handle this case too.
80
- return null;
76
+ return parent.stateNode.containerInfo;
77
+ case Portal:
78
+ return parent.stateNode.containerInfo;
79
}
80
parent = parent.return;
81
}
82
return null;
83
}
84
85
+ function isHostParent(fiber : Fiber) : boolean {
86
+ return (
87
+ fiber.tag === HostComponent ||
88
+ fiber.tag === HostContainer ||
89
+ fiber.tag === Portal
90
+ );
91
+ }
92
+
93
function getHostSibling(fiber : Fiber) : ?I {
94
// We're going to search forward into the tree until we find a sibling host
95
// node. Unfortunately, if multiple insertions are done in a row we have to
@@ -93,7 +99,7 @@ module.exports = function<T, P, I, TI, C>(
99
siblings: while (true) {
100
// If we didn't find anything, let's try the next sibling.
101
while (!node.sibling) {
96
- if (!node.return || node.return.tag === HostComponent) {
102
+ if (!node.return || isHostParent(node.return)) {
103
// If we pop out of the root or hit the parent the fiber we are the
104
// last sibling.
105
return null;
@@ -142,6 +148,10 @@ module.exports = function<T, P, I, TI, C>(
148
} else {
149
appendChild(parent, node.stateNode);
150
}
151
+ } else if (node.tag === Portal) {
152
+ // If the insertion itself is a portal, then we don't want to traverse
153
+ // down its children. Instead, we'll get insertions from each child in
154
+ // the portal directly.
155
} else if (node.child) {
156
// TODO: Coroutines need to visit the stateNode.
157
node = node.child;
@@ -201,6 +211,18 @@ module.exports = function<T, P, I, TI, C>(
211
if (parent) {
212
removeChild(parent, node.stateNode);
213
}
214
+ } else if (node.tag === Portal) {
215
+ // If this is a portal, then the parent is actually the portal itself.
216
+ // We need to keep track of which parent we're removing from.
217
+ // TODO: This uses a recursive call. We can get rid of that by mutating
218
+ // the parent binding and restoring it by searching for the host parent
219
+ // again when we pop past a portal.
220
+ const portalParent = node.stateNode.containerInfo;
221
+ let child = node.child;
222
+ while (child) {
223
+ unmountHostComponents(portalParent, child);
224
+ child = child.sibling;
225
+ }
226
} else {
227
commitUnmount(node);
228
if (node.child) {
@@ -260,11 +282,6 @@ module.exports = function<T, P, I, TI, C>(
282
detachRef(current);
283
return;
284
}
263
- case Portal: {
264
- const containerInfo : C = current.stateNode.containerInfo;
265
- updateContainer(containerInfo, null);
266
- return;
267
- }
285
}
286
}
287
@@ -274,14 +291,6 @@ module.exports = function<T, P, I, TI, C>(
291
detachRefIfNeeded(current, finishedWork);
292
return;
293
}
277
- case HostContainer: {
278
- // TODO: Attach children to root container.
279
- const children = finishedWork.output;
280
- const root : FiberRoot = finishedWork.stateNode;
281
- const containerInfo : C = root.containerInfo;
282
- updateContainer(containerInfo, children);
283
- return;
284
- }
294
case HostComponent: {
295
const instance : I = finishedWork.stateNode;
296
if (instance != null && current) {
@@ -303,10 +312,10 @@ module.exports = function<T, P, I, TI, C>(
312
commitTextUpdate(textInstance, oldText, newText);
313
return;
314
}
315
+ case HostContainer: {
316
+ return;
317
+ }
318
case Portal: {
307
- const children = finishedWork.child;
308
- const containerInfo : C = finishedWork.stateNode.containerInfo;
309
- updateContainer(containerInfo, children);
319
return;
320
}
321
default:
src/renderers/shared/fiber/ReactFiberCompleteWork.js
+3
-5
@@ -167,11 +167,8 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
167
fiberRoot.context = fiberRoot.pendingContext;
168
fiberRoot.pendingContext = null;
169
}
170
- // We don't know if a container has updated any children so we always
171
- // need to update it right now. We schedule this side-effect before
172
- // all the other side-effects in the subtree. We need to schedule it
173
- // before so that the entire tree is up-to-date before the life-cycles
174
- // are invoked.
170
+ // TODO: Only mark this as an update if we have any pending callbacks
171
+ // on it.
172
markUpdate(workInProgress);
173
return null;
174
}
@@ -253,6 +250,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
250
transferOutput(workInProgress.child, workInProgress);
251
return null;
252
case Portal:
253
+ // TODO: Only mark this as an update if we have any pending callbacks.
254
markUpdate(workInProgress);
255
workInProgress.output = null;
256
workInProgress.memoizedProps = workInProgress.pendingProps;
src/renderers/shared/fiber/ReactFiberReconciler.js
+3
-9
@@ -47,12 +47,6 @@ type OpaqueNode = Fiber;
47
48
export type HostConfig<T, P, I, TI, C> = {
49
50
- // TODO: We don't currently have a quick way to detect that children didn't
51
- // reorder so we host will always need to check the set. We should make a flag
52
- // or something so that it can bailout easily.
53
-
54
- updateContainer(containerInfo : C, children : HostChildren<I | TI>) : void,
55
-
50
createInstance(type : T, props : P, children : HostChildren<I | TI>, internalInstanceHandle : OpaqueNode) : I,
51
prepareUpdate(instance : I, oldProps : P, newProps : P) : boolean,
52
commitUpdate(instance : I, oldProps : P, newProps : P, internalInstanceHandle : OpaqueNode) : void,
@@ -60,9 +54,9 @@ export type HostConfig<T, P, I, TI, C> = {
54
createTextInstance(text : string, internalInstanceHandle : OpaqueNode) : TI,
55
commitTextUpdate(textInstance : TI, oldText : string, newText : string) : void,
56
63
- appendChild(parentInstance : I, child : I | TI) : void,
64
- insertBefore(parentInstance : I, child : I | TI, beforeChild : I | TI) : void,
65
- removeChild(parentInstance : I, child : I | TI) : void,
57
+ appendChild(parentInstance : I | C, child : I | TI) : void,
58
+ insertBefore(parentInstance : I | C, child : I | TI, beforeChild : I | TI) : void,
59
+ removeChild(parentInstance : I | C, child : I | TI) : void,
60
61
scheduleAnimationCallback(callback : () => void) : void,
62
scheduleDeferredCallback(callback : (deadline : Deadline) => void) : void,