-
Notifications
You must be signed in to change notification settings - Fork 47.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Fragment as a named export to React #10783
Changes from 33 commits
284efed
c26914b
adb7c61
3804288
f201879
3a21716
480e8d1
29e6472
e62c4b4
e0c0a1c
c74af40
1f9ef58
14d3a1b
6df523d
b4f17f6
6b374f8
7ceb631
53969d3
8ecb60c
ab1a58e
b7fff43
a6aca28
372a62a
8405465
cee245c
b3bac19
198ad8c
1a47984
5edee97
1539f19
367c7e6
59f6828
d29bd31
8c070e6
545d0b9
c8a0752
fe2dd4d
48bcaa7
b3864af
17ac4a2
27312d0
87ab859
f9443d1
97d1bdd
71252b9
8cbc93d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -102,6 +102,11 @@ const FAUX_ITERATOR_SYMBOL = '@@iterator'; // Before Symbol spec. | |
const REACT_ELEMENT_TYPE = | ||
(typeof Symbol === 'function' && Symbol.for && Symbol.for('react.element')) || | ||
0xeac7; | ||
const REACT_FRAGMENT_TYPE = | ||
(typeof Symbol === 'function' && | ||
Symbol.for && | ||
Symbol.for('react.fragment')) || | ||
0xeacb; | ||
|
||
function getIteratorFn(maybeIterable: ?any): ?() => ?Iterator<*> { | ||
if (maybeIterable === null || typeof maybeIterable === 'undefined') { | ||
|
@@ -375,27 +380,35 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { | |
element: ReactElement, | ||
expirationTime: ExpirationTime, | ||
): Fiber { | ||
if (current === null || current.type !== element.type) { | ||
// Insert | ||
const created = createFiberFromElement( | ||
element, | ||
returnFiber.internalContextTag, | ||
expirationTime, | ||
); | ||
created.ref = coerceRef(current, element); | ||
created.return = returnFiber; | ||
return created; | ||
} else { | ||
// TODO: Split these into branches based on typeof type | ||
if ( | ||
current !== null && | ||
(current.tag === Fragment | ||
? element.type === REACT_FRAGMENT_TYPE | ||
: current.type === element.type) | ||
) { | ||
// Move based on index | ||
const existing = useFiber(current, expirationTime); | ||
existing.ref = coerceRef(current, element); | ||
existing.pendingProps = element.props; | ||
existing.pendingProps = element.type === REACT_FRAGMENT_TYPE | ||
? element.props.children | ||
: element.props; | ||
existing.return = returnFiber; | ||
if (__DEV__) { | ||
existing._debugSource = element._source; | ||
existing._debugOwner = element._owner; | ||
} | ||
return existing; | ||
} else { | ||
// Insert | ||
const created = createFiberFromElement( | ||
element, | ||
returnFiber.internalContextTag, | ||
expirationTime, | ||
); | ||
created.ref = coerceRef(current, element); | ||
created.return = returnFiber; | ||
return created; | ||
} | ||
} | ||
|
||
|
@@ -483,13 +496,15 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { | |
current: Fiber | null, | ||
fragment: Iterable<*>, | ||
expirationTime: ExpirationTime, | ||
key: null | string, | ||
): Fiber { | ||
if (current === null || current.tag !== Fragment) { | ||
// Insert | ||
const created = createFiberFromFragment( | ||
fragment, | ||
returnFiber.internalContextTag, | ||
expirationTime, | ||
key, | ||
); | ||
created.return = returnFiber; | ||
return created; | ||
|
@@ -570,6 +585,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { | |
newChild, | ||
returnFiber.internalContextTag, | ||
expirationTime, | ||
null, | ||
); | ||
created.return = returnFiber; | ||
return created; | ||
|
@@ -616,6 +632,15 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { | |
switch (newChild.$$typeof) { | ||
case REACT_ELEMENT_TYPE: { | ||
if (newChild.key === key) { | ||
if (newChild.type === REACT_FRAGMENT_TYPE) { | ||
return updateFragment( | ||
returnFiber, | ||
oldFiber, | ||
newChild.props.children, | ||
expirationTime, | ||
key, | ||
); | ||
} | ||
return updateElement( | ||
returnFiber, | ||
oldFiber, | ||
|
@@ -666,12 +691,13 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { | |
} | ||
|
||
if (isArray(newChild) || getIteratorFn(newChild)) { | ||
// Fragments don't have keys so if the previous key is implicit we can | ||
// update it. | ||
if (key !== null) { | ||
return null; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
return updateFragment(returnFiber, oldFiber, newChild, expirationTime); | ||
return updateFragment( | ||
returnFiber, | ||
oldFiber, | ||
newChild, | ||
expirationTime, | ||
key, | ||
); | ||
} | ||
|
||
throwOnInvalidObjectType(returnFiber, newChild); | ||
|
@@ -712,6 +738,15 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { | |
existingChildren.get( | ||
newChild.key === null ? newIdx : newChild.key, | ||
) || null; | ||
if (newChild.type === REACT_FRAGMENT_TYPE) { | ||
return updateFragment( | ||
returnFiber, | ||
matchedFiber, | ||
newChild.props.children, | ||
expirationTime, | ||
newChild.key, | ||
); | ||
} | ||
return updateElement( | ||
returnFiber, | ||
matchedFiber, | ||
|
@@ -766,6 +801,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { | |
matchedFiber, | ||
newChild, | ||
expirationTime, | ||
null, | ||
); | ||
} | ||
|
||
|
@@ -1203,11 +1239,18 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { | |
// TODO: If key === null and child.key === null, then this only applies to | ||
// the first item in the list. | ||
if (child.key === key) { | ||
if (child.type === element.type) { | ||
// TODO: Split these into branches based on typeof type | ||
if ( | ||
child.tag === Fragment | ||
? element.type === REACT_FRAGMENT_TYPE | ||
: child.type === element.type | ||
) { | ||
deleteRemainingChildren(returnFiber, child.sibling); | ||
const existing = useFiber(child, expirationTime); | ||
existing.ref = coerceRef(child, element); | ||
existing.pendingProps = element.props; | ||
existing.pendingProps = element.type === REACT_FRAGMENT_TYPE | ||
? element.props.children | ||
: element.props; | ||
existing.return = returnFiber; | ||
if (__DEV__) { | ||
existing._debugSource = element._source; | ||
|
@@ -1342,9 +1385,140 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { | |
return created; | ||
} | ||
|
||
// A fork of reconcileChildFibers to be used when reconcileChildFibers | ||
// encounters a top level fragment to treat it as a set of children. | ||
// This function is not recursive. | ||
function reconcileChildFibersForTopLevelFragment( | ||
returnFiber: Fiber, | ||
currentFirstChild: Fiber | null, | ||
newChild: any, | ||
expirationTime: ExpirationTime, | ||
): Fiber | null { | ||
// This function is not recursive. | ||
// If the top level item is an array, we treat it as a set of children, | ||
// not as a fragment. Nested arrays on the other hand will be treated as | ||
// fragment nodes. Recursion happens at the normal flow. | ||
|
||
// Handle object types | ||
const isObject = typeof newChild === 'object' && newChild !== null; | ||
if (isObject) { | ||
switch (newChild.$$typeof) { | ||
case REACT_ELEMENT_TYPE: | ||
return placeSingleChild( | ||
reconcileSingleElement( | ||
returnFiber, | ||
currentFirstChild, | ||
newChild, | ||
expirationTime, | ||
), | ||
); | ||
case REACT_COROUTINE_TYPE: | ||
return placeSingleChild( | ||
reconcileSingleCoroutine( | ||
returnFiber, | ||
currentFirstChild, | ||
newChild, | ||
expirationTime, | ||
), | ||
); | ||
case REACT_YIELD_TYPE: | ||
return placeSingleChild( | ||
reconcileSingleYield( | ||
returnFiber, | ||
currentFirstChild, | ||
newChild, | ||
expirationTime, | ||
), | ||
); | ||
case REACT_PORTAL_TYPE: | ||
return placeSingleChild( | ||
reconcileSinglePortal( | ||
returnFiber, | ||
currentFirstChild, | ||
newChild, | ||
expirationTime, | ||
), | ||
); | ||
} | ||
} | ||
|
||
if (typeof newChild === 'string' || typeof newChild === 'number') { | ||
return placeSingleChild( | ||
reconcileSingleTextNode( | ||
returnFiber, | ||
currentFirstChild, | ||
'' + newChild, | ||
expirationTime, | ||
), | ||
); | ||
} | ||
|
||
if (isArray(newChild)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doesn't this mean that We should probably have a test for this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I gave you bad advice. The function you wanted to fork was There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bringing this up made me think about some other permutations of fragment and array combinations given our current desired behavior of top-level fragments behaving the same as a top-level array. I jotted some of them down here (with the ones I'm unsure about marked with |
||
return reconcileChildrenArray( | ||
returnFiber, | ||
currentFirstChild, | ||
newChild, | ||
expirationTime, | ||
); | ||
} | ||
|
||
if (getIteratorFn(newChild)) { | ||
return reconcileChildrenIterator( | ||
returnFiber, | ||
currentFirstChild, | ||
newChild, | ||
expirationTime, | ||
); | ||
} | ||
|
||
if (isObject) { | ||
throwOnInvalidObjectType(returnFiber, newChild); | ||
} | ||
|
||
if (__DEV__) { | ||
if (typeof newChild === 'function') { | ||
warnOnFunctionType(); | ||
} | ||
} | ||
if (typeof newChild === 'undefined') { | ||
// If the new child is undefined, and the return fiber is a composite | ||
// component, throw an error. If Fiber return types are disabled, | ||
// we already threw above. | ||
switch (returnFiber.tag) { | ||
case ClassComponent: { | ||
if (__DEV__) { | ||
const instance = returnFiber.stateNode; | ||
if (instance.render._isMockFunction) { | ||
// We allow auto-mocks to proceed as if they're returning null. | ||
break; | ||
} | ||
} | ||
} | ||
// Intentionally fall through to the next case, which handles both | ||
// functions and classes | ||
// eslint-disable-next-lined no-fallthrough | ||
case FunctionalComponent: { | ||
const Component = returnFiber.type; | ||
invariant( | ||
false, | ||
'%s(...): Nothing was returned from render. This usually means a ' + | ||
'return statement is missing. Or, to render nothing, ' + | ||
'return null.', | ||
Component.displayName || Component.name || 'Component', | ||
); | ||
} | ||
} | ||
} | ||
|
||
// Remaining cases are all treated as empty. | ||
return deleteRemainingChildren(returnFiber, currentFirstChild); | ||
} | ||
|
||
// This API will tag the children with the side-effect of the reconciliation | ||
// itself. They will be added to the side-effect list as we pass through the | ||
// children and the parent. | ||
// This function is forked at reconcileChildFibersForTopLevelFragment | ||
// Please reflect changes in this function to its fork as well | ||
function reconcileChildFibers( | ||
returnFiber: Fiber, | ||
currentFirstChild: Fiber | null, | ||
|
@@ -1361,6 +1535,17 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { | |
if (isObject) { | ||
switch (newChild.$$typeof) { | ||
case REACT_ELEMENT_TYPE: | ||
// This function recurses only on a top-level fragment, | ||
// so that it is treated as a set of children | ||
// Otherwise, we follow the normal flow. | ||
if (newChild.type === REACT_FRAGMENT_TYPE && newChild.key === null) { | ||
return reconcileChildFibersForTopLevelFragment( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I actually think you don't even need to fork anything. You just need to call |
||
returnFiber, | ||
currentFirstChild, | ||
newChild.props.children, | ||
expirationTime, | ||
); | ||
} | ||
return placeSingleChild( | ||
reconcileSingleElement( | ||
returnFiber, | ||
|
@@ -1369,7 +1554,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { | |
expirationTime, | ||
), | ||
); | ||
|
||
case REACT_COROUTINE_TYPE: | ||
return placeSingleChild( | ||
reconcileSingleCoroutine( | ||
|
@@ -1388,7 +1572,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { | |
expirationTime, | ||
), | ||
); | ||
|
||
case REACT_PORTAL_TYPE: | ||
return placeSingleChild( | ||
reconcileSinglePortal( | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why? Can you point me to discussion that explains this TODO?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#11290 It's to ensure correctness between type checking when comparing across functions, classes, strings, and potentially numbers/symbols.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should split based on
tag
, nottypeof
. It doesn't really matter if you're going to check the type anyway. Some of the other duplicates of this comment is confusing since in those cases you already do that branching.