Apply side-effects to host containers
This updates the host container root with new children. Currently, this is always called for updates because we don't track if any children reordered.
Sebastian Markbage committed
Jun 30, 2016 at 00:58 UTC
2f0ff6e9749dcdf30ff418338b17e2fc02f692c2
10 files changed
+213
-27
.flowconfig
+1
-1
@@ -34,4 +34,4 @@ suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(2[0-4]\\|1[0-9]\\|[0-9
34
suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy
35
36
[version]
37
-^0.26.0
37
+^0.27.0
package.json
+1
-1
@@ -43,7 +43,7 @@
43
"eslint-plugin-react-internal": "file:eslint-rules",
44
"fbjs": "^0.8.1",
45
"fbjs-scripts": "^0.6.0",
46
- "flow-bin": "^0.26.0",
46
+ "flow-bin": "^0.27.0",
47
"glob": "^6.0.1",
48
"grunt": "^0.4.5",
49
"grunt-cli": "^0.1.13",
src/renderers/noop/ReactNoop.js
+61
-4
@@ -27,18 +27,53 @@ var ReactFiberReconciler = require('ReactFiberReconciler');
27
var scheduledHighPriCallback = null;
28
var scheduledLowPriCallback = null;
29
30
+const TERMINAL_TAG = 99;
31
+
32
+type Container = { rootID: number, children: Array<Instance> };
33
type Props = { };
31
-type Instance = { id: number };
34
+type Instance = { tag: 99, type: string, id: number, children: Array<Instance> };
35
36
var instanceCounter = 0;
37
38
+function recursivelyAppendChildren(flatArray : Array<Instance>, child : HostChildren<Instance>) {
39
+ if (!child) {
40
+ return;
41
+ }
42
+ if (child.tag === TERMINAL_TAG) {
43
+ flatArray.push(child);
44
+ } else {
45
+ let node = child;
46
+ do {
47
+ recursivelyAppendChildren(flatArray, node.output);
48
+ } while (node = node.sibling);
49
+ }
50
+}
51
+
52
+function flattenChildren(children : HostChildren<Instance>) {
53
+ const flatArray = [];
54
+ recursivelyAppendChildren(flatArray, children);
55
+ return flatArray;
56
+}
57
+
58
var NoopRenderer = ReactFiberReconciler({
59
60
+ updateContainer(containerInfo : Container, children : HostChildren<Instance>) : void {
61
+ console.log('Update container #' + containerInfo.rootID);
62
+ containerInfo.children = flattenChildren(children);
63
+ },
64
+
65
createInstance(type : string, props : Props, children : HostChildren<Instance>) : Instance {
66
console.log('Create instance #' + instanceCounter);
39
- return {
40
- id: instanceCounter++
67
+ const inst = {
68
+ tag: TERMINAL_TAG,
69
+ id: instanceCounter++,
70
+ type: type,
71
+ children: flattenChildren(children),
72
};
73
+ // Hide from unit tests
74
+ Object.defineProperty(inst, 'tag', { value: inst.tag, enumerable: false });
75
+ Object.defineProperty(inst, 'id', { value: inst.id, enumerable: false });
76
+ return inst;
77
},
78
79
prepareUpdate(instance : Instance, oldProps : Props, newProps : Props, children : HostChildren<Instance>) : boolean {
@@ -48,6 +83,7 @@ var NoopRenderer = ReactFiberReconciler({
83
84
commitUpdate(instance : Instance, oldProps : Props, newProps : Props, children : HostChildren<Instance>) : void {
85
console.log('Commit update on #' + instance.id);
86
+ instance.children = flattenChildren(children);
87
},
88
89
deleteInstance(instance : Instance) : void {
@@ -64,13 +100,17 @@ var NoopRenderer = ReactFiberReconciler({
100
101
});
102
103
+var rootContainer = { rootID: 0, children: [] };
104
+
105
var root = null;
106
107
var ReactNoop = {
108
109
+ root: rootContainer,
110
+
111
render(element : ReactElement<any>) {
112
if (!root) {
73
- root = NoopRenderer.mountContainer(element, null);
113
+ root = NoopRenderer.mountContainer(element, rootContainer);
114
} else {
115
NoopRenderer.updateContainer(element, root);
116
}
@@ -115,6 +155,19 @@ var ReactNoop = {
155
console.log('Nothing rendered yet.');
156
return;
157
}
158
+
159
+ function logHostInstances(children: Array<Instance>, depth) {
160
+ for (var i = 0; i < children.length; i++) {
161
+ var child = children[i];
162
+ console.log(' '.repeat(depth) + '- ' + child.type + '#' + child.id);
163
+ logHostInstances(child.children, depth + 1);
164
+ }
165
+ }
166
+ function logContainer(container : Container, depth) {
167
+ console.log(' '.repeat(depth) + '- [root#' + container.rootID + ']');
168
+ logHostInstances(container.children, depth + 1);
169
+ }
170
+
171
function logFiber(fiber : Fiber, depth) {
172
console.log(' '.repeat(depth) + '- ' + (fiber.type ? fiber.type.name || fiber.type : '[root]'), '[' + fiber.pendingWorkPriority + (fiber.pendingProps ? '*' : '') + ']');
173
if (fiber.child) {
@@ -124,6 +177,10 @@ var ReactNoop = {
177
logFiber(fiber.sibling, depth);
178
}
179
}
180
+
181
+ console.log('HOST INSTANCES:');
182
+ logContainer(rootContainer, 0);
183
+ console.log('FIBERS:');
184
logFiber((root.stateNode : any).current, 0);
185
},
186
src/renderers/shared/fiber/ReactFiberBeginWork.js
+1
-1
@@ -34,7 +34,7 @@ var {
34
} = require('ReactPriorityLevel');
35
var { findNextUnitOfWorkAtPriority } = require('ReactFiberPendingWork');
36
37
-module.exports = function<T, P, I>(config : HostConfig<T, P, I>) {
37
+module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
38
39
function reconcileChildren(current, workInProgress, nextChildren) {
40
const priority = workInProgress.pendingWorkPriority;
src/renderers/shared/fiber/ReactFiberCommitWork.js
+15
-5
@@ -13,6 +13,7 @@
13
'use strict';
14
15
import type { Fiber } from 'ReactFiber';
16
+import type { FiberRoot } from 'ReactFiberRoot';
17
import type { HostConfig } from 'ReactFiberReconciler';
18
19
var ReactTypeOfWork = require('ReactTypeOfWork');
@@ -22,23 +23,31 @@ var {
23
HostComponent,
24
} = ReactTypeOfWork;
25
25
-module.exports = function<T, P, I>(config : HostConfig<T, P, I>) {
26
+module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
27
28
+ const updateContainer = config.updateContainer;
29
const commitUpdate = config.commitUpdate;
30
31
function commitWork(finishedWork : Fiber) : void {
32
switch (finishedWork.tag) {
31
- case ClassComponent:
33
+ case ClassComponent: {
34
// TODO: Fire componentDidMount/componentDidUpdate, update refs
35
return;
34
- case HostContainer:
36
+ }
37
+ case HostContainer: {
38
// TODO: Attach children to root container.
39
+ const children = finishedWork.output;
40
+ const root : FiberRoot = finishedWork.stateNode;
41
+ const containerInfo : C = root.containerInfo;
42
+ updateContainer(containerInfo, children);
43
return;
37
- case HostComponent:
44
+ }
45
+ case HostComponent: {
46
if (finishedWork.stateNode == null || !finishedWork.alternate) {
47
throw new Error('This should only be done during updates.');
48
}
41
- const children = finishedWork.output;
49
+ const child = (finishedWork.child : ?Fiber);
50
+ const children = (child && !child.sibling) ? (child.output : ?Fiber | I) : child;
51
const newProps = finishedWork.memoizedProps;
52
// If we have an alternate, that means this is an update and we need to
53
// schedule a side-effect to do the updates.
@@ -47,6 +56,7 @@ module.exports = function<T, P, I>(config : HostConfig<T, P, I>) {
56
const instance : I = finishedWork.stateNode;
57
commitUpdate(instance, oldProps, newProps, children);
58
return;
59
+ }
60
default:
61
throw new Error('This unit of work tag should not have side-effects.');
62
}
src/renderers/shared/fiber/ReactFiberCompleteWork.js
+19
-4
@@ -17,6 +17,8 @@ import type { Fiber } from 'ReactFiber';
17
import type { HostConfig } from 'ReactFiberReconciler';
18
import type { ReifiedYield } from 'ReactReifiedYield';
19
20
+import type { TypeOfWork } from 'ReactTypeOfWork';
21
+
22
var ReactChildFiber = require('ReactChildFiber');
23
var ReactTypeOfWork = require('ReactTypeOfWork');
24
var {
@@ -30,7 +32,7 @@ var {
32
YieldComponent,
33
} = ReactTypeOfWork;
34
33
-module.exports = function<T, P, I>(config : HostConfig<T, P, I>) {
35
+module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
36
37
const createInstance = config.createInstance;
38
const prepareUpdate = config.prepareUpdate;
@@ -120,12 +122,22 @@ module.exports = function<T, P, I>(config : HostConfig<T, P, I>) {
122
transferOutput(workInProgress.child, workInProgress);
123
return null;
124
case HostContainer:
125
+ transferOutput(workInProgress.child, workInProgress);
126
+ // We don't know if a container has updated any children so we always
127
+ // need to update it right now. We schedule this side-effect after
128
+ // all the other side-effects in the subtree.
129
+ // TODO: I just realized that this means that nodes are not in the DOM
130
+ // document when componentDidMount/Update fires. I'll need to change
131
+ // the order for host component effects to happen *before* their
132
+ // children I think.
133
+ markForPostEffect(workInProgress);
134
return null;
135
case HostComponent:
136
console.log('/host component', workInProgress.type);
126
- transferOutput(workInProgress.child, workInProgress);
127
- const children = workInProgress.output;
128
- const newProps = workInProgress.memoizedProps;
137
+ const child = (workInProgress.child : ?Fiber);
138
+ const children = (child && !child.sibling) ? (child.output : ?Fiber | I) : child;
139
+ const newProps = workInProgress.pendingProps;
140
+ workInProgress.memoizedProps = newProps;
141
if (workInProgress.alternate && workInProgress.stateNode != null) {
142
// If we have an alternate, that means this is an update and we need to
143
// schedule a side-effect to do the updates.
@@ -136,9 +148,12 @@ module.exports = function<T, P, I>(config : HostConfig<T, P, I>) {
148
// This returns true if there was something to update.
149
markForPostEffect(workInProgress);
150
}
151
+ workInProgress.output = instance;
152
} else {
153
const instance = createInstance(workInProgress.type, newProps, children);
154
+ // TODO: This seems like unnecessary duplication.
155
workInProgress.stateNode = instance;
156
+ workInProgress.output = instance;
157
}
158
return null;
159
case CoroutineComponent:
src/renderers/shared/fiber/ReactFiberReconciler.js
+15
-8
@@ -14,6 +14,7 @@
14
15
import type { Fiber } from 'ReactFiber';
16
import type { FiberRoot } from 'ReactFiberRoot';
17
+import type { TypeOfWork } from 'ReactTypeOfWork';
18
19
var { createFiberRoot } = require('ReactFiberRoot');
20
var ReactFiberScheduler = require('ReactFiberScheduler');
@@ -26,11 +27,17 @@ type Deadline = {
27
timeRemaining : () => number
28
};
29
29
-type HostChildNode<I> = { output: HostChildren<I>, sibling: ?HostChildNode<I> };
30
+type HostChildNode<I> = { tag: TypeOfWork, output: HostChildren<I>, sibling: any };
31
32
export type HostChildren<I> = null | void | I | HostChildNode<I>;
33
33
-export type HostConfig<T, P, I> = {
34
+export type HostConfig<T, P, I, C> = {
35
+
36
+ // TODO: We don't currently have a quick way to detect that children didn't
37
+ // reorder so we host will always need to check the set. We should make a flag
38
+ // or something so that it can bailout easily.
39
+
40
+ updateContainer(containerInfo : C, children : HostChildren<I>) : void;
41
42
createInstance(type : T, props : P, children : HostChildren<I>) : I,
43
prepareUpdate(instance : I, oldProps : P, newProps : P, children : HostChildren<I>) : bool,
@@ -44,22 +51,22 @@ export type HostConfig<T, P, I> = {
51
52
type OpaqueNode = Fiber;
53
47
-export type Reconciler = {
48
- mountContainer(element : ReactElement<any>, containerInfo : ?Object) : OpaqueNode,
54
+export type Reconciler<C> = {
55
+ mountContainer(element : ReactElement<any>, containerInfo : C) : OpaqueNode,
56
updateContainer(element : ReactElement<any>, container : OpaqueNode) : void,
57
unmountContainer(container : OpaqueNode) : void,
58
59
// Used to extract the return value from the initial render. Legacy API.
53
- getPublicRootInstance(container : OpaqueNode) : ?Object,
60
+ getPublicRootInstance(container : OpaqueNode) : (C | null),
61
};
62
56
-module.exports = function<T, P, I>(config : HostConfig<T, P, I>) : Reconciler {
63
+module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) : Reconciler<C> {
64
65
var { scheduleLowPriWork } = ReactFiberScheduler(config);
66
67
return {
68
62
- mountContainer(element : ReactElement<any>, containerInfo : ?Object) : OpaqueNode {
69
+ mountContainer(element : ReactElement<any>, containerInfo : C) : OpaqueNode {
70
const root = createFiberRoot(containerInfo);
71
const container = root.current;
72
// TODO: Use pending work/state instead of props.
@@ -94,7 +101,7 @@ module.exports = function<T, P, I>(config : HostConfig<T, P, I>) : Reconciler {
101
scheduleLowPriWork(root);
102
},
103
97
- getPublicRootInstance(container : OpaqueNode) : ?Object {
104
+ getPublicRootInstance(container : OpaqueNode) : (C | null) {
105
return null;
106
},
107
src/renderers/shared/fiber/ReactFiberRoot.js
+2
-2
@@ -18,7 +18,7 @@ const { createHostContainerFiber } = require('ReactFiber');
18
19
export type FiberRoot = {
20
// Any additional information from the host associated with this root.
21
- containerInfo: ?Object,
21
+ containerInfo: any,
22
// The currently active root fiber. This is the mutable root of the tree.
23
current: Fiber,
24
// Determines if this root has already been added to the schedule for work.
@@ -27,7 +27,7 @@ export type FiberRoot = {
27
nextScheduledRoot: ?FiberRoot,
28
};
29
30
-exports.createFiberRoot = function(containerInfo : ?Object) : FiberRoot {
30
+exports.createFiberRoot = function(containerInfo : any) : FiberRoot {
31
// Cyclic construction. This cheats the type system right now because
32
// stateNode is any.
33
const uninitializedFiber = createHostContainerFiber();
src/renderers/shared/fiber/ReactFiberScheduler.js
+1
-1
@@ -32,7 +32,7 @@ var {
32
33
var timeHeuristicForUnitOfWork = 1;
34
35
-module.exports = function<T, P, I>(config : HostConfig<T, P, I>) {
35
+module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
36
37
const { beginWork } = ReactFiberBeginWork(config);
38
const { completeWork } = ReactFiberCompleteWork(config);
src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js
new
+97
@@ -0,0 +1,97 @@
1
+/**
2
+ * Copyright 2013-present, Facebook, Inc.
3
+ * All rights reserved.
4
+ *
5
+ * This source code is licensed under the BSD-style license found in the
6
+ * LICENSE file in the root directory of this source tree. An additional grant
7
+ * of patent rights can be found in the PATENTS file in the same directory.
8
+ *
9
+ * @emails react-core
10
+ */
11
+
12
+'use strict';
13
+
14
+var React;
15
+var ReactNoop;
16
+
17
+describe('ReactIncremental', function() {
18
+ beforeEach(function() {
19
+ React = require('React');
20
+ ReactNoop = require('ReactNoop');
21
+ spyOn(console, 'log');
22
+ });
23
+
24
+ function div(...children) {
25
+ return { type: 'div', children };
26
+ }
27
+
28
+ function span(...children) {
29
+ return { type: 'span', children };
30
+ }
31
+
32
+ it('can update child nodes of a host instance', function() {
33
+
34
+ function Bar(props) {
35
+ return <span>{props.text}</span>;
36
+ }
37
+
38
+ function Foo(props) {
39
+ return (
40
+ <div>
41
+ <Bar text={props.text} />
42
+ {props.text === 'World' ? <Bar text={props.text} /> : null}
43
+ </div>
44
+ );
45
+ }
46
+
47
+ ReactNoop.render(<Foo text="Hello" />);
48
+ ReactNoop.flush();
49
+ expect(ReactNoop.root.children).toEqual([
50
+ div(span()),
51
+ ]);
52
+
53
+ ReactNoop.render(<Foo text="World" />);
54
+ ReactNoop.flush();
55
+ expect(ReactNoop.root.children).toEqual([
56
+ div(span(), span()),
57
+ ]);
58
+
59
+ });
60
+
61
+ it('does not update child nodes if a flush is aborted', function() {
62
+
63
+ function Bar(props) {
64
+ return <span>{props.text}</span>;
65
+ }
66
+
67
+ function Foo(props) {
68
+ return (
69
+ <div>
70
+ <div>
71
+ <Bar text={props.text} />
72
+ {props.text === 'Hello' ? <Bar text={props.text} /> : null}
73
+ </div>
74
+ <Bar text="Yo" />
75
+ </div>
76
+ );
77
+ }
78
+
79
+ ReactNoop.render(<Foo text="Hello" />);
80
+ ReactNoop.flush();
81
+ expect(ReactNoop.root.children).toEqual([
82
+ div(div(span(), span()), span()),
83
+ ]);
84
+
85
+ ReactNoop.render(<Foo text="World" />);
86
+ ReactNoop.flushLowPri(35);
87
+ expect(ReactNoop.root.children).toEqual([
88
+ div(div(span(), span()), span()),
89
+ ]);
90
+
91
+ });
92
+
93
+ // TODO: Test that side-effects are not cut off when a work in progress node
94
+ // moves to "current" without flushing due to having lower priority. Does this
95
+ // even happen? Maybe a child doesn't get processed because it is lower prio?
96
+
97
+});