Add Fragment fiber type
We currently treat nested arrays as a unique namespace from top level children. So that these won't share key namespaces. This adds a new fiber type that will represent the position of a fragment. This is only used for nested arrays. Even if you return an array from a composite component, we don't need this since they share namespace with a single top level component. This still doesn't implement the complete reconciliation algorthim in child fiber. That's coming later.
Sebastian Markbage committed
Sep 13, 2016 at 10:56 UTC
e0a305350b16f02857c1d6111f7a1a5c97e4bf25
8 files changed
+200
-45
src/renderers/shared/fiber/ReactChildFiber.js
+36
-40
@@ -30,6 +30,7 @@ var ReactReifiedYield = require('ReactReifiedYield');
30
const {
31
cloneFiber,
32
createFiberFromElement,
33
+ createFiberFromFragment,
34
createFiberFromCoroutine,
35
createFiberFromYield,
36
} = ReactFiber;
@@ -46,7 +47,7 @@ function ChildReconciler(shouldClone) {
47
returnFiber : Fiber,
48
existingChild : ?Fiber,
49
previousSibling : Fiber,
49
- newChildren,
50
+ newChildren : any,
51
priority : PriorityLevel
52
) : Fiber {
53
if (typeof newChildren !== 'object' || newChildren === null) {
@@ -98,35 +99,24 @@ function ChildReconciler(shouldClone) {
99
}
100
101
if (isArray(newChildren)) {
101
- let prev : Fiber = previousSibling;
102
- let existing : ?Fiber = existingChild;
103
- for (var i = 0; i < newChildren.length; i++) {
104
- var nextExisting = existing && existing.sibling;
105
- prev = createSubsequentChild(returnFiber, existing, prev, newChildren[i], priority);
106
- if (prev && existing) {
107
- // TODO: This is not correct because there could've been more
108
- // than one sibling consumed but I don't want to return a tuple.
109
- existing = nextExisting;
110
- }
111
- }
112
- return prev;
102
+ // Nested arrays have a special fiber type.
103
+ const fragment = createFiberFromFragment(newChildren, priority);
104
+ previousSibling.sibling = fragment;
105
+ fragment.return = returnFiber;
106
+ return fragment;
107
} else {
108
// TODO: Throw for unknown children.
109
return previousSibling;
110
}
111
}
112
119
- function createFirstChild(returnFiber, existingChild, newChildren, priority) {
113
+ function createFirstChild(returnFiber, existingChild, newChildren : any, priority) {
114
if (typeof newChildren !== 'object' || newChildren === null) {
115
return null;
116
}
117
118
switch (newChildren.$$typeof) {
119
case REACT_ELEMENT_TYPE: {
126
- /* $FlowFixMe(>=0.31.0): This is an unsafe cast. Consider adding a type
127
- * annotation to the `newChildren` param of this
128
- * function.
129
- */
120
const element = (newChildren : ReactElement<any>);
121
if (existingChild &&
122
element.type === existingChild.type &&
@@ -148,8 +138,6 @@ function ChildReconciler(shouldClone) {
138
}
139
140
case REACT_COROUTINE_TYPE: {
151
- /* $FlowFixMe(>=0.31.0): No 'handler' property found in object type
152
- */
141
const coroutine = (newChildren : ReactCoroutine);
142
const child = createFiberFromCoroutine(coroutine, priority);
143
child.return = returnFiber;
@@ -160,9 +148,6 @@ function ChildReconciler(shouldClone) {
148
// A yield results in a fragment fiber whose output is the continuation.
149
// TODO: When there is only a single child, we can optimize this to avoid
150
// the fragment.
163
- /* $FlowFixMe(>=0.31.0): No 'continuation' property found in object
164
- * type
165
- */
151
const yieldNode = (newChildren : ReactYield);
152
const reifiedYield = createReifiedYield(yieldNode);
153
const child = createFiberFromYield(yieldNode, priority);
@@ -173,14 +158,38 @@ function ChildReconciler(shouldClone) {
158
}
159
160
if (isArray(newChildren)) {
176
- var first : ?Fiber = null;
177
- var prev : ?Fiber = null;
178
- var existing : ?Fiber = existingChild;
161
+ // Nested arrays have a special fiber type.
162
+ const fragment = createFiberFromFragment(newChildren, priority);
163
+ fragment.return = returnFiber;
164
+ return fragment;
165
+ } else {
166
+ // TODO: Throw for unknown children.
167
+ return null;
168
+ }
169
+ }
170
+
171
+ // TODO: This API won't work because we'll need to transfer the side-effects of
172
+ // unmounting children to the returnFiber.
173
+ function reconcileChildFibers(
174
+ returnFiber : Fiber,
175
+ currentFirstChild : ?Fiber,
176
+ newChild : ReactNodeList,
177
+ priority : PriorityLevel
178
+ ) : ?Fiber {
179
+ // This function is not recursive.
180
+ // If the top level item is an array, we treat it as a set of children,
181
+ // not as a fragment. Nested arrays on the other hand will be treated as
182
+ // fragment nodes. Recursion happens at the normal flow.
183
+ if (isArray(newChild)) {
184
/* $FlowIssue(>=0.31.0) #12747709
185
*
186
* `Array.isArray` is matched syntactically for now until predicate
187
* support is complete.
188
*/
189
+ var newChildren : Array<*> = newChild;
190
+ var first : ?Fiber = null;
191
+ var prev : ?Fiber = null;
192
+ var existing : ?Fiber = currentFirstChild;
193
for (var i = 0; i < newChildren.length; i++) {
194
var nextExisting = existing && existing.sibling;
195
if (prev == null) {
@@ -196,21 +205,8 @@ function ChildReconciler(shouldClone) {
205
}
206
}
207
return first;
199
- } else {
200
- // TODO: Throw for unknown children.
201
- return null;
208
}
203
- }
204
-
205
- // TODO: This API won't work because we'll need to transfer the side-effects of
206
- // unmounting children to the returnFiber.
207
- function reconcileChildFibers(
208
- returnFiber : Fiber,
209
- currentFirstChild : ?Fiber,
210
- newChildren : ReactNodeList,
211
- priority : PriorityLevel
212
- ) : ?Fiber {
213
- return createFirstChild(returnFiber, currentFirstChild, newChildren, priority);
209
+ return createFirstChild(returnFiber, currentFirstChild, newChild, priority);
210
}
211
212
return reconcileChildFibers;
src/renderers/shared/fiber/ReactFiber.js
+12
@@ -12,6 +12,7 @@
12
13
'use strict';
14
15
+import type { ReactFragment } from 'ReactTypes';
16
import type { ReactCoroutine, ReactYield } from 'ReactCoroutine';
17
import type { TypeOfWork } from 'ReactTypeOfWork';
18
import type { PriorityLevel } from 'ReactPriorityLevel';
@@ -25,6 +26,7 @@ var {
26
HostComponent,
27
CoroutineComponent,
28
YieldComponent,
29
+ Fragment,
30
} = ReactTypeOfWork;
31
32
var {
@@ -245,6 +247,15 @@ exports.createFiberFromElement = function(element : ReactElement<*>, priorityLev
247
return fiber;
248
};
249
250
+exports.createFiberFromFragment = function(elements : ReactFragment, priorityLevel : PriorityLevel) {
251
+ // TODO: Consider supporting keyed fragments. Technically, we accidentally
252
+ // support that in the existing React.
253
+ const fiber = createFiber(Fragment, null);
254
+ fiber.pendingProps = elements;
255
+ fiber.pendingWorkPriority = priorityLevel;
256
+ return fiber;
257
+};
258
+
259
function createFiberFromElementType(type : mixed, key : null | string) {
260
let fiber;
261
if (typeof type === 'function') {
@@ -279,3 +290,4 @@ exports.createFiberFromYield = function(yieldNode : ReactYield, priorityLevel :
290
fiber.pendingProps = {};
291
return fiber;
292
};
293
+
src/renderers/shared/fiber/ReactFiberBeginWork.js
+16
@@ -36,6 +36,7 @@ var {
36
CoroutineComponent,
37
CoroutineHandlerPhase,
38
YieldComponent,
39
+ Fragment,
40
} = ReactTypeOfWork;
41
var {
42
NoWork,
@@ -97,6 +98,11 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>, getSchedu
98
markChildAsProgressed(current, workInProgress, priorityLevel);
99
}
100
101
+ function updateFragment(current, workInProgress) {
102
+ var nextChildren = workInProgress.pendingProps;
103
+ reconcileChildren(current, workInProgress, nextChildren);
104
+ }
105
+
106
function updateFunctionalComponent(current, workInProgress) {
107
var fn = workInProgress.type;
108
var props = workInProgress.pendingProps;
@@ -412,6 +418,16 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>, getSchedu
418
);
419
}
420
return null;
421
+ case Fragment:
422
+ updateFragment(current, workInProgress);
423
+ if (workInProgress.child) {
424
+ return beginWork(
425
+ workInProgress.child.alternate,
426
+ workInProgress.child,
427
+ priorityLevel
428
+ );
429
+ }
430
+ return null;
431
default:
432
throw new Error('Unknown unit of work tag');
433
}
src/renderers/shared/fiber/ReactFiberCompleteWork.js
+4
@@ -28,6 +28,7 @@ var {
28
CoroutineComponent,
29
CoroutineHandlerPhase,
30
YieldComponent,
31
+ Fragment,
32
} = ReactTypeOfWork;
33
34
module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
@@ -199,6 +200,9 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
200
case YieldComponent:
201
// Does nothing.
202
return null;
203
+ case Fragment:
204
+ transferOutput(workInProgress.child, workInProgress);
205
+ return null;
206
207
// Error cases
208
case IndeterminateComponent:
src/renderers/shared/fiber/ReactTypeOfWork.js
+2
-1
@@ -12,7 +12,7 @@
12
13
'use strict';
14
15
-export type TypeOfWork = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
15
+export type TypeOfWork = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
16
17
module.exports = {
18
IndeterminateComponent: 0, // Before we know whether it is functional or class
@@ -23,4 +23,5 @@ module.exports = {
23
CoroutineComponent: 5,
24
CoroutineHandlerPhase: 6,
25
YieldComponent: 7,
26
+ Fragment: 8,
27
};
src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js
+32
@@ -57,6 +57,38 @@ describe('ReactIncrementalSideEffects', () => {
57
58
});
59
60
+ it('can update child nodes of a fragment', function() {
61
+
62
+ function Bar(props) {
63
+ return <span>{props.text}</span>;
64
+ }
65
+
66
+ function Foo(props) {
67
+ return (
68
+ <div>
69
+ <Bar text={props.text} />
70
+ {props.text === 'World' ? [
71
+ <Bar key="a" text={props.text} />,
72
+ <div key="b" />,
73
+ ] : null}
74
+ </div>
75
+ );
76
+ }
77
+
78
+ ReactNoop.render(<Foo text="Hello" />);
79
+ ReactNoop.flush();
80
+ expect(ReactNoop.root.children).toEqual([
81
+ div(span()),
82
+ ]);
83
+
84
+ ReactNoop.render(<Foo text="World" />);
85
+ ReactNoop.flush();
86
+ expect(ReactNoop.root.children).toEqual([
87
+ div(span(), span(), div()),
88
+ ]);
89
+
90
+ });
91
+
92
it('does not update child nodes if a flush is aborted', () => {
93
94
function Bar(props) {
src/renderers/shared/fiber/__tests__/ReactTopLevelFragment-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
+// This is a new feature in Fiber so I put it in its own test file. It could
18
+// probably move to one of the other test files once it is official.
19
+describe('ReactTopLevelFragment', function() {
20
+ beforeEach(function() {
21
+ React = require('React');
22
+ ReactNoop = require('ReactNoop');
23
+ });
24
+
25
+ it('should render a simple fragment at the top of a component', function() {
26
+
27
+ function Fragment() {
28
+ return [<div key="a">Hello</div>, <div key="b">World</div>];
29
+ }
30
+ ReactNoop.render(<Fragment />);
31
+ ReactNoop.flush();
32
+
33
+ });
34
+
35
+ it('should preserve state when switching from a single child', function() {
36
+
37
+ var instance = null;
38
+
39
+ class Stateful extends React.Component {
40
+ render() {
41
+ instance = this;
42
+ return <div>Hello</div>;
43
+ }
44
+ }
45
+
46
+ function Fragment({ condition }) {
47
+ return condition ? <Stateful key="a" /> :
48
+ [<Stateful key="a" />, <div key="b">World</div>];
49
+ }
50
+ ReactNoop.render(<Fragment />);
51
+ ReactNoop.flush();
52
+
53
+ var instanceA = instance;
54
+
55
+ expect(instanceA).not.toBe(null);
56
+
57
+ ReactNoop.render(<Fragment condition={true} />);
58
+ ReactNoop.flush();
59
+
60
+ var instanceB = instance;
61
+
62
+ expect(instanceB).toBe(instanceA);
63
+
64
+ });
65
+
66
+ it('should not preserve state when switching to a nested array', function() {
67
+
68
+ var instance = null;
69
+
70
+ class Stateful extends React.Component {
71
+ render() {
72
+ instance = this;
73
+ return <div>Hello</div>;
74
+ }
75
+ }
76
+
77
+ function Fragment({ condition }) {
78
+ return condition ? <Stateful key="a" /> :
79
+ [[<Stateful key="a" />, <div key="b">World</div>], <div />];
80
+ }
81
+ ReactNoop.render(<Fragment />);
82
+ ReactNoop.flush();
83
+
84
+ var instanceA = instance;
85
+
86
+ expect(instanceA).not.toBe(null);
87
+
88
+ ReactNoop.render(<Fragment condition={true} />);
89
+ ReactNoop.flush();
90
+
91
+ var instanceB = instance;
92
+
93
+ expect(instanceB).not.toBe(instanceA);
94
+
95
+ });
96
+
97
+});
src/renderers/shared/fiber/isomorphic/ReactCoroutine.js
+1
-4
@@ -33,10 +33,7 @@ export type ReactCoroutine = {
33
children: any,
34
// This should be a more specific CoroutineHandler
35
handler: (props: any, yields: Array<ReifiedYield>) => ReactNodeList,
36
- /* $FlowFixMe(>=0.31.0): Which is it? mixed? Or Object? Must match
37
- * `ReactYield` type.
38
- */
39
- props: mixed,
36
+ props: any,
37
};
38
export type ReactYield = {
39
$$typeof: Symbol | number,