mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Add Fragment as a named export to React (#10783)
* Add Fragment as a named export to React * Remove extra tests for Fragment * Change React.Fragment export to be a string '#fragment' * Fix fragment special case to work with 1 child * Add single child test for fragment export * Move fragment definition to ReactEntry.js and render components for key warning tests * Inline createFiberFromElementType into createFiberFromElement * Update reconciliation to special case fragments * Use same semantics as implicit childsets for ReactFragment * Add more fragment state preservation tests * Export symbol instead of string for fragments * Fix rebase breakages * Re-apply prettier at 1.2.2 * Merge branches in updateElement * Remove unnecessary check * Re-use createFiberFromFragment for fragment case * Simplyify branches by adding type field to fragment fiber * Move branching logic for fragments to broader methods when possible. * Add more tests for fragments * Address Dan's feedback * Move REACT_FRAGMENT_TYPE into __DEV__ block for DCE * Change hex representation of REACT_FRAGMENT_TYPE to follow convention * Remove unnecessary branching and isArray checks * Update test for preserving children state when keys are same * Fix updateSlot bug and add more tests * Make fragment tests more robust by using ops pattern * Update jsx element validator to allow numbers and symbols * Remove type field from fragment fiber * Fork reconcileChildFibers instead of recursing * Use ternary if condition * Revamp fragment test suite: - Add more coverage to fragment tests - Use better names - Remove useless Fragment component inside tests - Remove useless tests so that tests are more concise * Check output of renderer in fragment tests to ensure no silly business despite states being preserved * Finish implementation of fragment reconciliation with desired behavior * Add reverse render direction for fragment tests * Remove unneeded fragment branch in updateElement * Add more test cases for ReactFragment * Handle childless fragment in reconciler * Support fragment flattening in SSR * Clean up ReactPartialRenderer * Warn when non-key and children props are passed to fragments * Add non-null key check back to updateSlot's array's case * Add test for positional reconciliation in fragments * Add warning for refs in fragments with stack trace
This commit is contained in:
@@ -403,6 +403,55 @@ describe('ReactDOMServerIntegration', () => {
|
||||
expect(parent.childNodes[2].tagName).toBe('P');
|
||||
});
|
||||
|
||||
itRenders('a fragment with one child', async render => {
|
||||
let e = await render(<React.Fragment><div>text1</div></React.Fragment>);
|
||||
let parent = e.parentNode;
|
||||
expect(parent.childNodes[0].tagName).toBe('DIV');
|
||||
});
|
||||
|
||||
itRenders('a fragment with several children', async render => {
|
||||
let Header = props => {
|
||||
return <p>header</p>;
|
||||
};
|
||||
let Footer = props => {
|
||||
return <React.Fragment><h2>footer</h2><h3>about</h3></React.Fragment>;
|
||||
};
|
||||
let e = await render(
|
||||
<React.Fragment>
|
||||
<div>text1</div>
|
||||
<span>text2</span>
|
||||
<Header />
|
||||
<Footer />
|
||||
</React.Fragment>,
|
||||
);
|
||||
let parent = e.parentNode;
|
||||
expect(parent.childNodes[0].tagName).toBe('DIV');
|
||||
expect(parent.childNodes[1].tagName).toBe('SPAN');
|
||||
expect(parent.childNodes[2].tagName).toBe('P');
|
||||
expect(parent.childNodes[3].tagName).toBe('H2');
|
||||
expect(parent.childNodes[4].tagName).toBe('H3');
|
||||
});
|
||||
|
||||
itRenders('a nested fragment', async render => {
|
||||
let e = await render(
|
||||
<React.Fragment>
|
||||
<React.Fragment>
|
||||
<div>text1</div>
|
||||
</React.Fragment>
|
||||
<span>text2</span>
|
||||
<React.Fragment>
|
||||
<React.Fragment>
|
||||
<React.Fragment>{null}<p /></React.Fragment>{false}
|
||||
</React.Fragment>
|
||||
</React.Fragment>
|
||||
</React.Fragment>,
|
||||
);
|
||||
let parent = e.parentNode;
|
||||
expect(parent.childNodes[0].tagName).toBe('DIV');
|
||||
expect(parent.childNodes[1].tagName).toBe('SPAN');
|
||||
expect(parent.childNodes[2].tagName).toBe('P');
|
||||
});
|
||||
|
||||
itRenders('an iterable', async render => {
|
||||
const threeDivIterable = {
|
||||
'@@iterator': function() {
|
||||
@@ -435,6 +484,7 @@ describe('ReactDOMServerIntegration', () => {
|
||||
// but server returns empty HTML. So we compare parent text.
|
||||
expect((await render(<div>{''}</div>)).textContent).toBe('');
|
||||
|
||||
expect(await render(<React.Fragment />)).toBe(null);
|
||||
expect(await render([])).toBe(null);
|
||||
expect(await render(false)).toBe(null);
|
||||
expect(await render(true)).toBe(null);
|
||||
|
||||
+59
-27
@@ -31,6 +31,12 @@ var escapeTextContentForBrowser = require('../shared/escapeTextContentForBrowser
|
||||
var isCustomComponent = require('../shared/isCustomComponent');
|
||||
var omittedCloseTags = require('../shared/omittedCloseTags');
|
||||
|
||||
var REACT_FRAGMENT_TYPE =
|
||||
(typeof Symbol === 'function' &&
|
||||
Symbol.for &&
|
||||
Symbol.for('react.fragment')) ||
|
||||
0xeacb;
|
||||
|
||||
// Based on reading the React.Children implementation. TODO: type this somewhere?
|
||||
type ReactNode = string | number | ReactElement;
|
||||
type FlatReactChildren = Array<null | ReactNode>;
|
||||
@@ -206,6 +212,22 @@ function getNonChildrenInnerMarkup(props) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function flattenTopLevelChildren(children: mixed): FlatReactChildren {
|
||||
if (!React.isValidElement(children)) {
|
||||
return toArray(children);
|
||||
}
|
||||
const element = ((children: any): ReactElement);
|
||||
if (element.type !== REACT_FRAGMENT_TYPE) {
|
||||
return [element];
|
||||
}
|
||||
const fragmentChildren = element.props.children;
|
||||
if (!React.isValidElement(fragmentChildren)) {
|
||||
return toArray(fragmentChildren);
|
||||
}
|
||||
const fragmentChildElement = ((fragmentChildren: any): ReactElement);
|
||||
return [fragmentChildElement];
|
||||
}
|
||||
|
||||
function flattenOptionChildren(children: mixed): string {
|
||||
var content = '';
|
||||
// Flatten children and warn if they aren't strings or numbers;
|
||||
@@ -482,14 +504,8 @@ class ReactDOMServerRenderer {
|
||||
makeStaticMarkup: boolean;
|
||||
|
||||
constructor(children: mixed, makeStaticMarkup: boolean) {
|
||||
var flatChildren;
|
||||
if (React.isValidElement(children)) {
|
||||
// Safe because we just checked it's an element.
|
||||
var element = ((children: any): ReactElement);
|
||||
flatChildren = [element];
|
||||
} else {
|
||||
flatChildren = toArray(children);
|
||||
}
|
||||
const flatChildren = flattenTopLevelChildren(children);
|
||||
|
||||
var topFrame: Frame = {
|
||||
// Assume all trees start in the HTML namespace (not totally true, but
|
||||
// this is what we did historically)
|
||||
@@ -569,26 +585,42 @@ class ReactDOMServerRenderer {
|
||||
({child: nextChild, context} = resolve(child, context));
|
||||
if (nextChild === null || nextChild === false) {
|
||||
return '';
|
||||
} else {
|
||||
if (React.isValidElement(nextChild)) {
|
||||
// Safe because we just checked it's an element.
|
||||
var nextElement = ((nextChild: any): ReactElement);
|
||||
return this.renderDOM(nextElement, context, parentNamespace);
|
||||
} else {
|
||||
var nextChildren = toArray(nextChild);
|
||||
var frame: Frame = {
|
||||
domNamespace: parentNamespace,
|
||||
children: nextChildren,
|
||||
childIndex: 0,
|
||||
context: context,
|
||||
footer: '',
|
||||
};
|
||||
if (__DEV__) {
|
||||
((frame: any): FrameDev).debugElementStack = [];
|
||||
}
|
||||
this.stack.push(frame);
|
||||
return '';
|
||||
} else if (!React.isValidElement(nextChild)) {
|
||||
const nextChildren = toArray(nextChild);
|
||||
const frame: Frame = {
|
||||
domNamespace: parentNamespace,
|
||||
children: nextChildren,
|
||||
childIndex: 0,
|
||||
context: context,
|
||||
footer: '',
|
||||
};
|
||||
if (__DEV__) {
|
||||
((frame: any): FrameDev).debugElementStack = [];
|
||||
}
|
||||
this.stack.push(frame);
|
||||
return '';
|
||||
} else if (
|
||||
((nextChild: any): ReactElement).type === REACT_FRAGMENT_TYPE
|
||||
) {
|
||||
const nextChildren = toArray(
|
||||
((nextChild: any): ReactElement).props.children,
|
||||
);
|
||||
const frame: Frame = {
|
||||
domNamespace: parentNamespace,
|
||||
children: nextChildren,
|
||||
childIndex: 0,
|
||||
context: context,
|
||||
footer: '',
|
||||
};
|
||||
if (__DEV__) {
|
||||
((frame: any): FrameDev).debugElementStack = [];
|
||||
}
|
||||
this.stack.push(frame);
|
||||
return '';
|
||||
} else {
|
||||
// Safe because we just checked it's an element.
|
||||
var nextElement = ((nextChild: any): ReactElement);
|
||||
return this.renderDOM(nextElement, context, parentNamespace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -512,7 +512,8 @@ var ReactNoop = {
|
||||
log(
|
||||
' '.repeat(depth) +
|
||||
'- ' +
|
||||
(fiber.type ? fiber.type.name || fiber.type : '[root]'),
|
||||
// need to explicitly coerce Symbol to a string
|
||||
(fiber.type ? fiber.type.name || fiber.type.toString() : '[root]'),
|
||||
'[' + fiber.expirationTime + (fiber.pendingProps ? '*' : '') + ']',
|
||||
);
|
||||
if (fiber.updateQueue) {
|
||||
|
||||
+103
-33
@@ -103,14 +103,17 @@ const FAUX_ITERATOR_SYMBOL = '@@iterator'; // Before Symbol spec.
|
||||
var REACT_ELEMENT_TYPE;
|
||||
var REACT_CALL_TYPE;
|
||||
var REACT_RETURN_TYPE;
|
||||
var REACT_FRAGMENT_TYPE;
|
||||
if (typeof Symbol === 'function' && Symbol.for) {
|
||||
REACT_ELEMENT_TYPE = Symbol.for('react.element');
|
||||
REACT_CALL_TYPE = Symbol.for('react.call');
|
||||
REACT_RETURN_TYPE = Symbol.for('react.return');
|
||||
REACT_FRAGMENT_TYPE = Symbol.for('react.fragment');
|
||||
} else {
|
||||
REACT_ELEMENT_TYPE = 0xeac7;
|
||||
REACT_CALL_TYPE = 0xeac8;
|
||||
REACT_RETURN_TYPE = 0xeac9;
|
||||
REACT_FRAGMENT_TYPE = 0xeacb;
|
||||
}
|
||||
|
||||
function getIteratorFn(maybeIterable: ?any): ?() => ?Iterator<*> {
|
||||
@@ -385,17 +388,7 @@ 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 {
|
||||
if (current !== null && current.type === element.type) {
|
||||
// Move based on index
|
||||
const existing = useFiber(current, expirationTime);
|
||||
existing.ref = coerceRef(current, element);
|
||||
@@ -406,6 +399,16 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -493,6 +496,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
|
||||
current: Fiber | null,
|
||||
fragment: Iterable<*>,
|
||||
expirationTime: ExpirationTime,
|
||||
key: null | string,
|
||||
): Fiber {
|
||||
if (current === null || current.tag !== Fragment) {
|
||||
// Insert
|
||||
@@ -500,6 +504,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
|
||||
fragment,
|
||||
returnFiber.internalContextTag,
|
||||
expirationTime,
|
||||
key,
|
||||
);
|
||||
created.return = returnFiber;
|
||||
return created;
|
||||
@@ -533,14 +538,25 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
|
||||
if (typeof newChild === 'object' && newChild !== null) {
|
||||
switch (newChild.$$typeof) {
|
||||
case REACT_ELEMENT_TYPE: {
|
||||
const created = createFiberFromElement(
|
||||
newChild,
|
||||
returnFiber.internalContextTag,
|
||||
expirationTime,
|
||||
);
|
||||
created.ref = coerceRef(null, newChild);
|
||||
created.return = returnFiber;
|
||||
return created;
|
||||
if (newChild.type === REACT_FRAGMENT_TYPE) {
|
||||
const created = createFiberFromFragment(
|
||||
newChild.props.children,
|
||||
returnFiber.internalContextTag,
|
||||
expirationTime,
|
||||
newChild.key,
|
||||
);
|
||||
created.return = returnFiber;
|
||||
return created;
|
||||
} else {
|
||||
const created = createFiberFromElement(
|
||||
newChild,
|
||||
returnFiber.internalContextTag,
|
||||
expirationTime,
|
||||
);
|
||||
created.ref = coerceRef(null, newChild);
|
||||
created.return = returnFiber;
|
||||
return created;
|
||||
}
|
||||
}
|
||||
|
||||
case REACT_CALL_TYPE: {
|
||||
@@ -580,6 +596,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
|
||||
newChild,
|
||||
returnFiber.internalContextTag,
|
||||
expirationTime,
|
||||
null,
|
||||
);
|
||||
created.return = returnFiber;
|
||||
return created;
|
||||
@@ -626,6 +643,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,
|
||||
@@ -676,12 +702,17 @@ 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;
|
||||
}
|
||||
return updateFragment(returnFiber, oldFiber, newChild, expirationTime);
|
||||
|
||||
return updateFragment(
|
||||
returnFiber,
|
||||
oldFiber,
|
||||
newChild,
|
||||
expirationTime,
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
throwOnInvalidObjectType(returnFiber, newChild);
|
||||
@@ -722,6 +753,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,
|
||||
@@ -776,6 +816,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
|
||||
matchedFiber,
|
||||
newChild,
|
||||
expirationTime,
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1213,11 +1254,17 @@ 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) {
|
||||
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;
|
||||
@@ -1234,14 +1281,25 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
|
||||
child = child.sibling;
|
||||
}
|
||||
|
||||
const created = createFiberFromElement(
|
||||
element,
|
||||
returnFiber.internalContextTag,
|
||||
expirationTime,
|
||||
);
|
||||
created.ref = coerceRef(currentFirstChild, element);
|
||||
created.return = returnFiber;
|
||||
return created;
|
||||
if (element.type === REACT_FRAGMENT_TYPE) {
|
||||
const created = createFiberFromFragment(
|
||||
element.props.children,
|
||||
returnFiber.internalContextTag,
|
||||
expirationTime,
|
||||
element.key,
|
||||
);
|
||||
created.return = returnFiber;
|
||||
return created;
|
||||
} else {
|
||||
const created = createFiberFromElement(
|
||||
element,
|
||||
returnFiber.internalContextTag,
|
||||
expirationTime,
|
||||
);
|
||||
created.ref = coerceRef(currentFirstChild, element);
|
||||
created.return = returnFiber;
|
||||
return created;
|
||||
}
|
||||
}
|
||||
|
||||
function reconcileSingleCall(
|
||||
@@ -1366,8 +1424,21 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
|
||||
// not as a fragment. Nested arrays on the other hand will be treated as
|
||||
// fragment nodes. Recursion happens at the normal flow.
|
||||
|
||||
// Handle top level unkeyed fragments as if they were arrays.
|
||||
// This leads to an ambiguity between <>{[...]}</> and <>...</>.
|
||||
// We treat the ambiguous cases above the same.
|
||||
if (
|
||||
typeof newChild === 'object' &&
|
||||
newChild !== null &&
|
||||
newChild.type === REACT_FRAGMENT_TYPE &&
|
||||
newChild.key === null
|
||||
) {
|
||||
newChild = newChild.props.children;
|
||||
}
|
||||
|
||||
// Handle object types
|
||||
const isObject = typeof newChild === 'object' && newChild !== null;
|
||||
|
||||
if (isObject) {
|
||||
switch (newChild.$$typeof) {
|
||||
case REACT_ELEMENT_TYPE:
|
||||
@@ -1398,7 +1469,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
|
||||
expirationTime,
|
||||
),
|
||||
);
|
||||
|
||||
case REACT_PORTAL_TYPE:
|
||||
return placeSingleChild(
|
||||
reconcileSinglePortal(
|
||||
|
||||
+37
-49
@@ -301,62 +301,18 @@ exports.createFiberFromElement = function(
|
||||
owner = element._owner;
|
||||
}
|
||||
|
||||
const fiber = createFiberFromElementType(
|
||||
element.type,
|
||||
element.key,
|
||||
internalContextTag,
|
||||
owner,
|
||||
);
|
||||
fiber.pendingProps = element.props;
|
||||
fiber.expirationTime = expirationTime;
|
||||
|
||||
if (__DEV__) {
|
||||
fiber._debugSource = element._source;
|
||||
fiber._debugOwner = element._owner;
|
||||
}
|
||||
|
||||
return fiber;
|
||||
};
|
||||
|
||||
exports.createFiberFromFragment = function(
|
||||
elements: ReactFragment,
|
||||
internalContextTag: TypeOfInternalContext,
|
||||
expirationTime: ExpirationTime,
|
||||
): Fiber {
|
||||
// TODO: Consider supporting keyed fragments. Technically, we accidentally
|
||||
// support that in the existing React.
|
||||
const fiber = createFiber(Fragment, null, internalContextTag);
|
||||
fiber.pendingProps = elements;
|
||||
fiber.expirationTime = expirationTime;
|
||||
return fiber;
|
||||
};
|
||||
|
||||
exports.createFiberFromText = function(
|
||||
content: string,
|
||||
internalContextTag: TypeOfInternalContext,
|
||||
expirationTime: ExpirationTime,
|
||||
): Fiber {
|
||||
const fiber = createFiber(HostText, null, internalContextTag);
|
||||
fiber.pendingProps = content;
|
||||
fiber.expirationTime = expirationTime;
|
||||
return fiber;
|
||||
};
|
||||
|
||||
function createFiberFromElementType(
|
||||
type: mixed,
|
||||
key: null | string,
|
||||
internalContextTag: TypeOfInternalContext,
|
||||
debugOwner: null | Fiber,
|
||||
): Fiber {
|
||||
let fiber;
|
||||
const {type, key} = element;
|
||||
if (typeof type === 'function') {
|
||||
fiber = shouldConstruct(type)
|
||||
? createFiber(ClassComponent, key, internalContextTag)
|
||||
: createFiber(IndeterminateComponent, key, internalContextTag);
|
||||
fiber.type = type;
|
||||
fiber.pendingProps = element.props;
|
||||
} else if (typeof type === 'string') {
|
||||
fiber = createFiber(HostComponent, key, internalContextTag);
|
||||
fiber.type = type;
|
||||
fiber.pendingProps = element.props;
|
||||
} else if (
|
||||
typeof type === 'object' &&
|
||||
type !== null &&
|
||||
@@ -369,6 +325,7 @@ function createFiberFromElementType(
|
||||
// we don't know if we can reuse that fiber or if we need to clone it.
|
||||
// There is probably a clever way to restructure this.
|
||||
fiber = ((type: any): Fiber);
|
||||
fiber.pendingProps = element.props;
|
||||
} else {
|
||||
let info = '';
|
||||
if (__DEV__) {
|
||||
@@ -382,7 +339,7 @@ function createFiberFromElementType(
|
||||
' You likely forgot to export your component from the file ' +
|
||||
"it's defined in.";
|
||||
}
|
||||
const ownerName = debugOwner ? getComponentName(debugOwner) : null;
|
||||
const ownerName = owner ? getComponentName(owner) : null;
|
||||
if (ownerName) {
|
||||
info += '\n\nCheck the render method of `' + ownerName + '`.';
|
||||
}
|
||||
@@ -395,10 +352,41 @@ function createFiberFromElementType(
|
||||
info,
|
||||
);
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
fiber._debugSource = element._source;
|
||||
fiber._debugOwner = element._owner;
|
||||
}
|
||||
|
||||
fiber.expirationTime = expirationTime;
|
||||
|
||||
return fiber;
|
||||
};
|
||||
|
||||
function createFiberFromFragment(
|
||||
elements: ReactFragment,
|
||||
internalContextTag: TypeOfInternalContext,
|
||||
expirationTime: ExpirationTime,
|
||||
key: null | string,
|
||||
): Fiber {
|
||||
const fiber = createFiber(Fragment, key, internalContextTag);
|
||||
fiber.pendingProps = elements;
|
||||
fiber.expirationTime = expirationTime;
|
||||
return fiber;
|
||||
}
|
||||
|
||||
exports.createFiberFromElementType = createFiberFromElementType;
|
||||
exports.createFiberFromFragment = createFiberFromFragment;
|
||||
|
||||
exports.createFiberFromText = function(
|
||||
content: string,
|
||||
internalContextTag: TypeOfInternalContext,
|
||||
expirationTime: ExpirationTime,
|
||||
): Fiber {
|
||||
const fiber = createFiber(HostText, null, internalContextTag);
|
||||
fiber.pendingProps = content;
|
||||
fiber.expirationTime = expirationTime;
|
||||
return fiber;
|
||||
};
|
||||
|
||||
exports.createFiberFromHostInstanceForDeletion = function(): Fiber {
|
||||
const fiber = createFiber(HostComponent, null, NoContext);
|
||||
|
||||
@@ -0,0 +1,721 @@
|
||||
/**
|
||||
* Copyright (c) 2013-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @emails react-core
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
let React;
|
||||
let ReactNoop;
|
||||
|
||||
describe('ReactFragment', () => {
|
||||
beforeEach(function() {
|
||||
jest.resetModules();
|
||||
React = require('react');
|
||||
ReactNoop = require('react-noop-renderer');
|
||||
});
|
||||
|
||||
function span(prop) {
|
||||
return {type: 'span', children: [], prop};
|
||||
}
|
||||
|
||||
function text(val) {
|
||||
return {text: val};
|
||||
}
|
||||
|
||||
function div(...children) {
|
||||
children = children.map(c => (typeof c === 'string' ? {text: c} : c));
|
||||
return {type: 'div', children, prop: undefined};
|
||||
}
|
||||
|
||||
it('should render a single child via noop renderer', () => {
|
||||
const element = (
|
||||
<React.Fragment>
|
||||
<span>foo</span>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
ReactNoop.render(element);
|
||||
ReactNoop.flush();
|
||||
|
||||
expect(ReactNoop.getChildren()).toEqual([span()]);
|
||||
});
|
||||
|
||||
it('should render zero children via noop renderer', () => {
|
||||
const element = <React.Fragment />;
|
||||
|
||||
ReactNoop.render(element);
|
||||
ReactNoop.flush();
|
||||
|
||||
expect(ReactNoop.getChildren()).toEqual([]);
|
||||
});
|
||||
|
||||
it('should render multiple children via noop renderer', () => {
|
||||
const element = (
|
||||
<React.Fragment>
|
||||
hello <span>world</span>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
ReactNoop.render(element);
|
||||
ReactNoop.flush();
|
||||
|
||||
expect(ReactNoop.getChildren()).toEqual([text('hello '), span()]);
|
||||
});
|
||||
|
||||
it('should render an iterable via noop renderer', () => {
|
||||
const element = (
|
||||
<React.Fragment>
|
||||
{new Set([<span key="a">hi</span>, <span key="b">bye</span>])}
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
ReactNoop.render(element);
|
||||
ReactNoop.flush();
|
||||
|
||||
expect(ReactNoop.getChildren()).toEqual([span(), span()]);
|
||||
});
|
||||
|
||||
it('should preserve state of children with 1 level nesting', function() {
|
||||
var ops = [];
|
||||
|
||||
class Stateful extends React.Component {
|
||||
componentDidUpdate() {
|
||||
ops.push('Update Stateful');
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>Hello</div>;
|
||||
}
|
||||
}
|
||||
|
||||
function Foo({condition}) {
|
||||
return condition
|
||||
? <Stateful key="a" />
|
||||
: <React.Fragment>
|
||||
<Stateful key="a" />
|
||||
<div key="b">World</div>
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
ReactNoop.render(<Foo condition={true} />);
|
||||
ReactNoop.flush();
|
||||
|
||||
ReactNoop.render(<Foo condition={false} />);
|
||||
ReactNoop.flush();
|
||||
|
||||
expect(ops).toEqual(['Update Stateful']);
|
||||
expect(ReactNoop.getChildren()).toEqual([div(), div()]);
|
||||
|
||||
ReactNoop.render(<Foo condition={true} />);
|
||||
ReactNoop.flush();
|
||||
|
||||
expect(ops).toEqual(['Update Stateful', 'Update Stateful']);
|
||||
expect(ReactNoop.getChildren()).toEqual([div()]);
|
||||
});
|
||||
|
||||
it('should preserve state between top-level fragments', function() {
|
||||
var ops = [];
|
||||
|
||||
class Stateful extends React.Component {
|
||||
componentDidUpdate() {
|
||||
ops.push('Update Stateful');
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>Hello</div>;
|
||||
}
|
||||
}
|
||||
|
||||
function Foo({condition}) {
|
||||
return condition
|
||||
? <React.Fragment>
|
||||
<Stateful />
|
||||
</React.Fragment>
|
||||
: <React.Fragment>
|
||||
<Stateful />
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
ReactNoop.render(<Foo condition={true} />);
|
||||
ReactNoop.flush();
|
||||
|
||||
ReactNoop.render(<Foo condition={false} />);
|
||||
ReactNoop.flush();
|
||||
|
||||
expect(ops).toEqual(['Update Stateful']);
|
||||
expect(ReactNoop.getChildren()).toEqual([div()]);
|
||||
|
||||
ReactNoop.render(<Foo condition={true} />);
|
||||
ReactNoop.flush();
|
||||
|
||||
expect(ops).toEqual(['Update Stateful', 'Update Stateful']);
|
||||
expect(ReactNoop.getChildren()).toEqual([div()]);
|
||||
});
|
||||
|
||||
it('should preserve state of children nested at same level', function() {
|
||||
var ops = [];
|
||||
|
||||
class Stateful extends React.Component {
|
||||
componentDidUpdate() {
|
||||
ops.push('Update Stateful');
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>Hello</div>;
|
||||
}
|
||||
}
|
||||
|
||||
function Foo({condition}) {
|
||||
return condition
|
||||
? <React.Fragment>
|
||||
<React.Fragment>
|
||||
<React.Fragment>
|
||||
<Stateful key="a" />
|
||||
</React.Fragment>
|
||||
</React.Fragment>
|
||||
</React.Fragment>
|
||||
: <React.Fragment>
|
||||
<React.Fragment>
|
||||
<React.Fragment>
|
||||
<div />
|
||||
<Stateful key="a" />
|
||||
</React.Fragment>
|
||||
</React.Fragment>
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
ReactNoop.render(<Foo condition={true} />);
|
||||
ReactNoop.flush();
|
||||
|
||||
ReactNoop.render(<Foo condition={false} />);
|
||||
ReactNoop.flush();
|
||||
|
||||
expect(ops).toEqual(['Update Stateful']);
|
||||
expect(ReactNoop.getChildren()).toEqual([div(), div()]);
|
||||
|
||||
ReactNoop.render(<Foo condition={true} />);
|
||||
ReactNoop.flush();
|
||||
|
||||
expect(ops).toEqual(['Update Stateful', 'Update Stateful']);
|
||||
expect(ReactNoop.getChildren()).toEqual([div()]);
|
||||
});
|
||||
|
||||
it('should not preserve state in non-top-level fragment nesting', function() {
|
||||
var ops = [];
|
||||
|
||||
class Stateful extends React.Component {
|
||||
componentDidUpdate() {
|
||||
ops.push('Update Stateful');
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>Hello</div>;
|
||||
}
|
||||
}
|
||||
|
||||
function Foo({condition}) {
|
||||
return condition
|
||||
? <React.Fragment>
|
||||
<React.Fragment><Stateful key="a" /></React.Fragment>
|
||||
</React.Fragment>
|
||||
: <React.Fragment><Stateful key="a" /></React.Fragment>;
|
||||
}
|
||||
|
||||
ReactNoop.render(<Foo condition={true} />);
|
||||
ReactNoop.flush();
|
||||
|
||||
ReactNoop.render(<Foo condition={false} />);
|
||||
ReactNoop.flush();
|
||||
|
||||
expect(ops).toEqual([]);
|
||||
expect(ReactNoop.getChildren()).toEqual([div()]);
|
||||
|
||||
ReactNoop.render(<Foo condition={true} />);
|
||||
ReactNoop.flush();
|
||||
|
||||
expect(ops).toEqual([]);
|
||||
expect(ReactNoop.getChildren()).toEqual([div()]);
|
||||
});
|
||||
|
||||
it('should not preserve state of children if nested 2 levels without siblings', function() {
|
||||
var ops = [];
|
||||
|
||||
class Stateful extends React.Component {
|
||||
componentDidUpdate() {
|
||||
ops.push('Update Stateful');
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>Hello</div>;
|
||||
}
|
||||
}
|
||||
|
||||
function Foo({condition}) {
|
||||
return condition
|
||||
? <Stateful key="a" />
|
||||
: <React.Fragment>
|
||||
<React.Fragment>
|
||||
<Stateful key="a" />
|
||||
</React.Fragment>
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
ReactNoop.render(<Foo condition={true} />);
|
||||
ReactNoop.flush();
|
||||
|
||||
ReactNoop.render(<Foo condition={false} />);
|
||||
ReactNoop.flush();
|
||||
|
||||
expect(ops).toEqual([]);
|
||||
expect(ReactNoop.getChildren()).toEqual([div()]);
|
||||
|
||||
ReactNoop.render(<Foo condition={true} />);
|
||||
ReactNoop.flush();
|
||||
|
||||
expect(ops).toEqual([]);
|
||||
expect(ReactNoop.getChildren()).toEqual([div()]);
|
||||
});
|
||||
|
||||
it('should not preserve state of children if nested 2 levels with siblings', function() {
|
||||
var ops = [];
|
||||
|
||||
class Stateful extends React.Component {
|
||||
componentDidUpdate() {
|
||||
ops.push('Update Stateful');
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>Hello</div>;
|
||||
}
|
||||
}
|
||||
|
||||
function Foo({condition}) {
|
||||
return condition
|
||||
? <Stateful key="a" />
|
||||
: <React.Fragment>
|
||||
<React.Fragment>
|
||||
<Stateful key="a" />
|
||||
</React.Fragment>
|
||||
<div />
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
ReactNoop.render(<Foo condition={true} />);
|
||||
ReactNoop.flush();
|
||||
|
||||
ReactNoop.render(<Foo condition={false} />);
|
||||
ReactNoop.flush();
|
||||
|
||||
expect(ops).toEqual([]);
|
||||
expect(ReactNoop.getChildren()).toEqual([div(), div()]);
|
||||
|
||||
ReactNoop.render(<Foo condition={true} />);
|
||||
ReactNoop.flush();
|
||||
|
||||
expect(ops).toEqual([]);
|
||||
expect(ReactNoop.getChildren()).toEqual([div()]);
|
||||
});
|
||||
|
||||
it('should preserve state between array nested in fragment and fragment', function() {
|
||||
var ops = [];
|
||||
|
||||
class Stateful extends React.Component {
|
||||
componentDidUpdate() {
|
||||
ops.push('Update Stateful');
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>Hello</div>;
|
||||
}
|
||||
}
|
||||
|
||||
function Foo({condition}) {
|
||||
return condition
|
||||
? <React.Fragment>
|
||||
<Stateful key="a" />
|
||||
</React.Fragment>
|
||||
: <React.Fragment>
|
||||
{[<Stateful key="a" />]}
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
ReactNoop.render(<Foo condition={true} />);
|
||||
ReactNoop.flush();
|
||||
|
||||
ReactNoop.render(<Foo condition={false} />);
|
||||
ReactNoop.flush();
|
||||
|
||||
expect(ops).toEqual(['Update Stateful']);
|
||||
expect(ReactNoop.getChildren()).toEqual([div()]);
|
||||
|
||||
ReactNoop.render(<Foo condition={true} />);
|
||||
ReactNoop.flush();
|
||||
|
||||
expect(ops).toEqual(['Update Stateful', 'Update Stateful']);
|
||||
expect(ReactNoop.getChildren()).toEqual([div()]);
|
||||
});
|
||||
|
||||
it('should preserve state between top level fragment and array', function() {
|
||||
var ops = [];
|
||||
|
||||
class Stateful extends React.Component {
|
||||
componentDidUpdate() {
|
||||
ops.push('Update Stateful');
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>Hello</div>;
|
||||
}
|
||||
}
|
||||
|
||||
function Foo({condition}) {
|
||||
return condition
|
||||
? [<Stateful key="a" />]
|
||||
: <React.Fragment>
|
||||
<Stateful key="a" />
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
ReactNoop.render(<Foo condition={true} />);
|
||||
ReactNoop.flush();
|
||||
|
||||
ReactNoop.render(<Foo condition={false} />);
|
||||
ReactNoop.flush();
|
||||
|
||||
expect(ops).toEqual(['Update Stateful']);
|
||||
expect(ReactNoop.getChildren()).toEqual([div()]);
|
||||
|
||||
ReactNoop.render(<Foo condition={true} />);
|
||||
ReactNoop.flush();
|
||||
|
||||
expect(ops).toEqual(['Update Stateful', 'Update Stateful']);
|
||||
expect(ReactNoop.getChildren()).toEqual([div()]);
|
||||
});
|
||||
|
||||
it('should not preserve state between array nested in fragment and double nested fragment', function() {
|
||||
var ops = [];
|
||||
|
||||
class Stateful extends React.Component {
|
||||
componentDidUpdate() {
|
||||
ops.push('Update Stateful');
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>Hello</div>;
|
||||
}
|
||||
}
|
||||
|
||||
function Foo({condition}) {
|
||||
return condition
|
||||
? <React.Fragment>{[<Stateful key="a" />]}</React.Fragment>
|
||||
: <React.Fragment>
|
||||
<React.Fragment><Stateful key="a" /></React.Fragment>
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
ReactNoop.render(<Foo condition={true} />);
|
||||
ReactNoop.flush();
|
||||
|
||||
ReactNoop.render(<Foo condition={false} />);
|
||||
ReactNoop.flush();
|
||||
|
||||
expect(ops).toEqual([]);
|
||||
expect(ReactNoop.getChildren()).toEqual([div()]);
|
||||
|
||||
ReactNoop.render(<Foo condition={true} />);
|
||||
ReactNoop.flush();
|
||||
|
||||
expect(ops).toEqual([]);
|
||||
expect(ReactNoop.getChildren()).toEqual([div()]);
|
||||
});
|
||||
|
||||
it('should not preserve state between array nested in fragment and double nested array', function() {
|
||||
var ops = [];
|
||||
|
||||
class Stateful extends React.Component {
|
||||
componentDidUpdate() {
|
||||
ops.push('Update Stateful');
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>Hello</div>;
|
||||
}
|
||||
}
|
||||
|
||||
function Foo({condition}) {
|
||||
return condition
|
||||
? <React.Fragment>{[<Stateful key="a" />]}</React.Fragment>
|
||||
: [[<Stateful key="a" />]];
|
||||
}
|
||||
|
||||
ReactNoop.render(<Foo condition={true} />);
|
||||
ReactNoop.flush();
|
||||
|
||||
ReactNoop.render(<Foo condition={false} />);
|
||||
ReactNoop.flush();
|
||||
|
||||
expect(ops).toEqual([]);
|
||||
expect(ReactNoop.getChildren()).toEqual([div()]);
|
||||
|
||||
ReactNoop.render(<Foo condition={true} />);
|
||||
ReactNoop.flush();
|
||||
|
||||
expect(ops).toEqual([]);
|
||||
expect(ReactNoop.getChildren()).toEqual([div()]);
|
||||
});
|
||||
|
||||
it('should preserve state between double nested fragment and double nested array', function() {
|
||||
var ops = [];
|
||||
|
||||
class Stateful extends React.Component {
|
||||
componentDidUpdate() {
|
||||
ops.push('Update Stateful');
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>Hello</div>;
|
||||
}
|
||||
}
|
||||
|
||||
function Foo({condition}) {
|
||||
return condition
|
||||
? <React.Fragment>
|
||||
<React.Fragment><Stateful key="a" /></React.Fragment>
|
||||
</React.Fragment>
|
||||
: [[<Stateful key="a" />]];
|
||||
}
|
||||
|
||||
ReactNoop.render(<Foo condition={true} />);
|
||||
ReactNoop.flush();
|
||||
|
||||
ReactNoop.render(<Foo condition={false} />);
|
||||
ReactNoop.flush();
|
||||
|
||||
expect(ops).toEqual(['Update Stateful']);
|
||||
expect(ReactNoop.getChildren()).toEqual([div()]);
|
||||
|
||||
ReactNoop.render(<Foo condition={true} />);
|
||||
ReactNoop.flush();
|
||||
|
||||
expect(ops).toEqual(['Update Stateful', 'Update Stateful']);
|
||||
expect(ReactNoop.getChildren()).toEqual([div()]);
|
||||
});
|
||||
|
||||
it('should not preserve state of children when the keys are different', function() {
|
||||
var ops = [];
|
||||
|
||||
class Stateful extends React.Component {
|
||||
componentDidUpdate() {
|
||||
ops.push('Update Stateful');
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>Hello</div>;
|
||||
}
|
||||
}
|
||||
|
||||
function Foo({condition}) {
|
||||
return condition
|
||||
? <React.Fragment key="a">
|
||||
<Stateful />
|
||||
</React.Fragment>
|
||||
: <React.Fragment key="b">
|
||||
<Stateful />
|
||||
<span>World</span>
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
ReactNoop.render(<Foo condition={true} />);
|
||||
ReactNoop.flush();
|
||||
|
||||
ReactNoop.render(<Foo condition={false} />);
|
||||
ReactNoop.flush();
|
||||
|
||||
expect(ops).toEqual([]);
|
||||
expect(ReactNoop.getChildren()).toEqual([div(), span()]);
|
||||
|
||||
ReactNoop.render(<Foo condition={true} />);
|
||||
ReactNoop.flush();
|
||||
|
||||
expect(ops).toEqual([]);
|
||||
expect(ReactNoop.getChildren()).toEqual([div()]);
|
||||
});
|
||||
|
||||
it('should not preserve state between unkeyed and keyed fragment', function() {
|
||||
var ops = [];
|
||||
|
||||
class Stateful extends React.Component {
|
||||
componentDidUpdate() {
|
||||
ops.push('Update Stateful');
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>Hello</div>;
|
||||
}
|
||||
}
|
||||
|
||||
function Foo({condition}) {
|
||||
return condition
|
||||
? <React.Fragment key="a">
|
||||
<Stateful />
|
||||
</React.Fragment>
|
||||
: <React.Fragment>
|
||||
<Stateful />
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
ReactNoop.render(<Foo condition={true} />);
|
||||
ReactNoop.flush();
|
||||
|
||||
ReactNoop.render(<Foo condition={false} />);
|
||||
ReactNoop.flush();
|
||||
|
||||
expect(ops).toEqual([]);
|
||||
expect(ReactNoop.getChildren()).toEqual([div()]);
|
||||
|
||||
ReactNoop.render(<Foo condition={true} />);
|
||||
ReactNoop.flush();
|
||||
|
||||
expect(ops).toEqual([]);
|
||||
expect(ReactNoop.getChildren()).toEqual([div()]);
|
||||
});
|
||||
|
||||
it('should preserve state with reordering in multiple levels', function() {
|
||||
var ops = [];
|
||||
|
||||
class Stateful extends React.Component {
|
||||
componentDidUpdate() {
|
||||
ops.push('Update Stateful');
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>Hello</div>;
|
||||
}
|
||||
}
|
||||
|
||||
function Foo({condition}) {
|
||||
return condition
|
||||
? <div>
|
||||
<React.Fragment key="c">
|
||||
<span>foo</span>
|
||||
<div key="b">
|
||||
<Stateful key="a" />
|
||||
</div>
|
||||
</React.Fragment>
|
||||
<span>boop</span>
|
||||
</div>
|
||||
: <div>
|
||||
<span>beep</span>
|
||||
<React.Fragment key="c">
|
||||
<div key="b">
|
||||
<Stateful key="a" />
|
||||
</div>
|
||||
<span>bar</span>
|
||||
</React.Fragment>
|
||||
</div>;
|
||||
}
|
||||
|
||||
ReactNoop.render(<Foo condition={true} />);
|
||||
ReactNoop.flush();
|
||||
|
||||
ReactNoop.render(<Foo condition={false} />);
|
||||
ReactNoop.flush();
|
||||
|
||||
expect(ops).toEqual(['Update Stateful']);
|
||||
expect(ReactNoop.getChildren()).toEqual([div(span(), div(div()), span())]);
|
||||
|
||||
ReactNoop.render(<Foo condition={true} />);
|
||||
ReactNoop.flush();
|
||||
|
||||
expect(ops).toEqual(['Update Stateful', 'Update Stateful']);
|
||||
expect(ReactNoop.getChildren()).toEqual([div(span(), div(div()), span())]);
|
||||
});
|
||||
|
||||
it('should not preserve state when switching to a keyed fragment to an array', function() {
|
||||
spyOn(console, 'error');
|
||||
var ops = [];
|
||||
|
||||
class Stateful extends React.Component {
|
||||
componentDidUpdate() {
|
||||
ops.push('Update Stateful');
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>Hello</div>;
|
||||
}
|
||||
}
|
||||
|
||||
function Foo({condition}) {
|
||||
return condition
|
||||
? <div>
|
||||
{<React.Fragment key="foo"><Stateful /></React.Fragment>}<span />
|
||||
</div>
|
||||
: <div>{[<Stateful />]}<span /></div>;
|
||||
}
|
||||
|
||||
ReactNoop.render(<Foo condition={true} />);
|
||||
ReactNoop.flush();
|
||||
|
||||
ReactNoop.render(<Foo condition={false} />);
|
||||
ReactNoop.flush();
|
||||
|
||||
expect(ops).toEqual([]);
|
||||
expect(ReactNoop.getChildren()).toEqual([div(div(), span())]);
|
||||
|
||||
ReactNoop.render(<Foo condition={true} />);
|
||||
ReactNoop.flush();
|
||||
|
||||
expect(ops).toEqual([]);
|
||||
expect(ReactNoop.getChildren()).toEqual([div(div(), span())]);
|
||||
expectDev(console.error.calls.count()).toBe(1);
|
||||
expectDev(console.error.calls.argsFor(0)[0]).toContain(
|
||||
'Each child in an array or iterator should have a unique "key" prop.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should preserve state when it does not change positions', function() {
|
||||
spyOn(console, 'error');
|
||||
var ops = [];
|
||||
|
||||
class Stateful extends React.Component {
|
||||
componentDidUpdate() {
|
||||
ops.push('Update Stateful');
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>Hello</div>;
|
||||
}
|
||||
}
|
||||
|
||||
function Foo({condition}) {
|
||||
return condition
|
||||
? [<span />, <React.Fragment><Stateful /></React.Fragment>]
|
||||
: [<span />, <React.Fragment><Stateful /></React.Fragment>];
|
||||
}
|
||||
|
||||
ReactNoop.render(<Foo condition={true} />);
|
||||
ReactNoop.flush();
|
||||
|
||||
ReactNoop.render(<Foo condition={false} />);
|
||||
ReactNoop.flush();
|
||||
|
||||
expect(ops).toEqual(['Update Stateful']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span(), div()]);
|
||||
|
||||
ReactNoop.render(<Foo condition={true} />);
|
||||
ReactNoop.flush();
|
||||
|
||||
expect(ops).toEqual(['Update Stateful', 'Update Stateful']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span(), div()]);
|
||||
expectDev(console.error.calls.count()).toBe(3);
|
||||
for (let errorIndex = 0; errorIndex < 3; ++errorIndex) {
|
||||
expectDev(console.error.calls.argsFor(errorIndex)[0]).toContain(
|
||||
'Each child in an array or iterator should have a unique "key" prop.',
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -25,6 +25,12 @@ if (__DEV__) {
|
||||
cloneElement = ReactElementValidator.cloneElement;
|
||||
}
|
||||
|
||||
const REACT_FRAGMENT_TYPE =
|
||||
(typeof Symbol === 'function' &&
|
||||
Symbol.for &&
|
||||
Symbol.for('react.fragment')) ||
|
||||
0xeacb;
|
||||
|
||||
var React = {
|
||||
Children: {
|
||||
map: ReactChildren.map,
|
||||
@@ -37,6 +43,7 @@ var React = {
|
||||
Component: ReactBaseClasses.Component,
|
||||
PureComponent: ReactBaseClasses.PureComponent,
|
||||
unstable_AsyncComponent: ReactBaseClasses.AsyncComponent,
|
||||
Fragment: REACT_FRAGMENT_TYPE,
|
||||
|
||||
createElement: createElement,
|
||||
cloneElement: cloneElement,
|
||||
|
||||
@@ -35,6 +35,8 @@ if (__DEV__) {
|
||||
return '#text';
|
||||
} else if (typeof element.type === 'string') {
|
||||
return element.type;
|
||||
} else if (element.type === REACT_FRAGMENT_TYPE) {
|
||||
return 'React.Fragment';
|
||||
} else {
|
||||
return element.type.displayName || element.type.name || 'Unknown';
|
||||
}
|
||||
@@ -54,6 +56,14 @@ if (__DEV__) {
|
||||
stack += ReactDebugCurrentFrame.getStackAddendum() || '';
|
||||
return stack;
|
||||
};
|
||||
|
||||
var REACT_FRAGMENT_TYPE =
|
||||
(typeof Symbol === 'function' &&
|
||||
Symbol.for &&
|
||||
Symbol.for('react.fragment')) ||
|
||||
0xeacb;
|
||||
|
||||
var VALID_FRAGMENT_PROPS = new Map([['children', true], ['key', true]]);
|
||||
}
|
||||
|
||||
var ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator;
|
||||
@@ -226,9 +236,44 @@ function validatePropTypes(element) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a fragment, validate that it can only be provided with fragment props
|
||||
* @param {ReactElement} fragment
|
||||
*/
|
||||
function validateFragmentProps(fragment) {
|
||||
currentlyValidatingElement = fragment;
|
||||
|
||||
for (const key of Object.keys(fragment.props)) {
|
||||
if (!VALID_FRAGMENT_PROPS.has(key)) {
|
||||
warning(
|
||||
false,
|
||||
'Invalid prop `%s` supplied to `React.Fragment`. ' +
|
||||
'React.Fragment can only have `key` and `children` props.%s',
|
||||
key,
|
||||
getStackAddendum(),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (fragment.ref !== null) {
|
||||
warning(
|
||||
false,
|
||||
'Invalid attribute `ref` supplied to `React.Fragment`.%s',
|
||||
getStackAddendum(),
|
||||
);
|
||||
}
|
||||
|
||||
currentlyValidatingElement = null;
|
||||
}
|
||||
|
||||
var ReactElementValidator = {
|
||||
createElement: function(type, props, children) {
|
||||
var validType = typeof type === 'string' || typeof type === 'function';
|
||||
var validType =
|
||||
typeof type === 'string' ||
|
||||
typeof type === 'function' ||
|
||||
typeof type === 'symbol' ||
|
||||
typeof type === 'number';
|
||||
// We warn in this case but don't throw. We expect the element creation to
|
||||
// succeed and there will likely be errors in render.
|
||||
if (!validType) {
|
||||
@@ -282,7 +327,11 @@ var ReactElementValidator = {
|
||||
}
|
||||
}
|
||||
|
||||
validatePropTypes(element);
|
||||
if (typeof type === 'symbol' && type === REACT_FRAGMENT_TYPE) {
|
||||
validateFragmentProps(element);
|
||||
} else {
|
||||
validatePropTypes(element);
|
||||
}
|
||||
|
||||
return element;
|
||||
},
|
||||
|
||||
@@ -267,10 +267,9 @@ describe('ReactElementValidator', () => {
|
||||
React.createElement(undefined);
|
||||
React.createElement(null);
|
||||
React.createElement(true);
|
||||
React.createElement(123);
|
||||
React.createElement({x: 17});
|
||||
React.createElement({});
|
||||
expectDev(console.error.calls.count()).toBe(6);
|
||||
expectDev(console.error.calls.count()).toBe(5);
|
||||
expectDev(console.error.calls.argsFor(0)[0]).toBe(
|
||||
'Warning: React.createElement: type is invalid -- expected a string ' +
|
||||
'(for built-in components) or a class/function (for composite ' +
|
||||
@@ -288,23 +287,18 @@ describe('ReactElementValidator', () => {
|
||||
'components) but got: boolean.',
|
||||
);
|
||||
expectDev(console.error.calls.argsFor(3)[0]).toBe(
|
||||
'Warning: React.createElement: type is invalid -- expected a string ' +
|
||||
'(for built-in components) or a class/function (for composite ' +
|
||||
'components) but got: number.',
|
||||
);
|
||||
expectDev(console.error.calls.argsFor(4)[0]).toBe(
|
||||
'Warning: React.createElement: type is invalid -- expected a string ' +
|
||||
'(for built-in components) or a class/function (for composite ' +
|
||||
'components) but got: object.',
|
||||
);
|
||||
expectDev(console.error.calls.argsFor(5)[0]).toBe(
|
||||
expectDev(console.error.calls.argsFor(4)[0]).toBe(
|
||||
'Warning: React.createElement: type is invalid -- expected a string ' +
|
||||
'(for built-in components) or a class/function (for composite ' +
|
||||
'components) but got: object. You likely forgot to export your ' +
|
||||
"component from the file it's defined in.",
|
||||
);
|
||||
React.createElement('div');
|
||||
expectDev(console.error.calls.count()).toBe(6);
|
||||
expectDev(console.error.calls.count()).toBe(5);
|
||||
});
|
||||
|
||||
it('includes the owner name when passing null, undefined, boolean, or number', () => {
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
|
||||
// TODO: All these warnings should become static errors using Flow instead
|
||||
// of dynamic errors when using JSX with Flow.
|
||||
|
||||
var React;
|
||||
var ReactDOM;
|
||||
var ReactTestUtils;
|
||||
@@ -51,7 +50,9 @@ describe('ReactJSXElementValidator', () => {
|
||||
it('warns for keys for arrays of elements in children position', () => {
|
||||
spyOn(console, 'error');
|
||||
|
||||
void <Component>{[<Component />, <Component />]}</Component>;
|
||||
ReactTestUtils.renderIntoDocument(
|
||||
<Component>{[<Component />, <Component />]}</Component>,
|
||||
);
|
||||
|
||||
expectDev(console.error.calls.count()).toBe(1);
|
||||
expectDev(console.error.calls.argsFor(0)[0]).toContain(
|
||||
@@ -84,6 +85,49 @@ describe('ReactJSXElementValidator', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('warns for fragments with illegal attributes', () => {
|
||||
spyOn(console, 'error');
|
||||
|
||||
class Foo extends React.Component {
|
||||
render() {
|
||||
return <React.Fragment a={1} b={2}>hello</React.Fragment>;
|
||||
}
|
||||
}
|
||||
|
||||
ReactTestUtils.renderIntoDocument(<Foo />);
|
||||
|
||||
expectDev(console.error.calls.count()).toBe(1);
|
||||
expectDev(console.error.calls.argsFor(0)[0]).toContain('Invalid prop `');
|
||||
expectDev(console.error.calls.argsFor(0)[0]).toContain(
|
||||
'` supplied to `React.Fragment`. React.Fragment ' +
|
||||
'can only have `key` and `children` props.',
|
||||
);
|
||||
});
|
||||
|
||||
it('warns for fragments with refs', () => {
|
||||
spyOn(console, 'error');
|
||||
|
||||
class Foo extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment
|
||||
ref={bar => {
|
||||
this.foo = bar;
|
||||
}}>
|
||||
hello
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ReactTestUtils.renderIntoDocument(<Foo />);
|
||||
|
||||
expectDev(console.error.calls.count()).toBe(1);
|
||||
expectDev(console.error.calls.argsFor(0)[0]).toContain(
|
||||
'Invalid attribute `ref` supplied to `React.Fragment`.',
|
||||
);
|
||||
});
|
||||
|
||||
it('warns for keys for iterables of elements in rest args', () => {
|
||||
spyOn(console, 'error');
|
||||
|
||||
@@ -99,7 +143,7 @@ describe('ReactJSXElementValidator', () => {
|
||||
},
|
||||
};
|
||||
|
||||
void <Component>{iterable}</Component>;
|
||||
ReactTestUtils.renderIntoDocument(<Component>{iterable}</Component>);
|
||||
|
||||
expectDev(console.error.calls.count()).toBe(1);
|
||||
expectDev(console.error.calls.argsFor(0)[0]).toContain(
|
||||
@@ -107,17 +151,43 @@ describe('ReactJSXElementValidator', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('does not warns for arrays of elements with keys', () => {
|
||||
it('does not warn for fragments of multiple elements without keys', () => {
|
||||
ReactTestUtils.renderIntoDocument(
|
||||
<React.Fragment>
|
||||
<span>1</span>
|
||||
<span>2</span>
|
||||
</React.Fragment>,
|
||||
);
|
||||
});
|
||||
|
||||
it('warns for fragments of multiple elements with same key', () => {
|
||||
spyOn(console, 'error');
|
||||
|
||||
void (
|
||||
<Component>{[<Component key="#1" />, <Component key="#2" />]}</Component>
|
||||
ReactTestUtils.renderIntoDocument(
|
||||
<React.Fragment>
|
||||
<span key="a">1</span>
|
||||
<span key="a">2</span>
|
||||
<span key="b">3</span>
|
||||
</React.Fragment>,
|
||||
);
|
||||
|
||||
expectDev(console.error.calls.count()).toBe(1);
|
||||
expectDev(console.error.calls.argsFor(0)[0]).toContain(
|
||||
'Encountered two children with the same key, `a`.',
|
||||
);
|
||||
});
|
||||
|
||||
it('does not warn for arrays of elements with keys', () => {
|
||||
spyOn(console, 'error');
|
||||
|
||||
ReactTestUtils.renderIntoDocument(
|
||||
<Component>{[<Component key="#1" />, <Component key="#2" />]}</Component>,
|
||||
);
|
||||
|
||||
expectDev(console.error.calls.count()).toBe(0);
|
||||
});
|
||||
|
||||
it('does not warns for iterable elements with keys', () => {
|
||||
it('does not warn for iterable elements with keys', () => {
|
||||
spyOn(console, 'error');
|
||||
|
||||
var iterable = {
|
||||
@@ -135,7 +205,7 @@ describe('ReactJSXElementValidator', () => {
|
||||
},
|
||||
};
|
||||
|
||||
void <Component>{iterable}</Component>;
|
||||
ReactTestUtils.renderIntoDocument(<Component>{iterable}</Component>);
|
||||
|
||||
expectDev(console.error.calls.count()).toBe(0);
|
||||
});
|
||||
@@ -156,25 +226,22 @@ describe('ReactJSXElementValidator', () => {
|
||||
};
|
||||
iterable.entries = iterable['@@iterator'];
|
||||
|
||||
void <Component>{iterable}</Component>;
|
||||
ReactTestUtils.renderIntoDocument(<Component>{iterable}</Component>);
|
||||
|
||||
expectDev(console.error.calls.count()).toBe(0);
|
||||
});
|
||||
|
||||
it('does not warn when the element is directly as children', () => {
|
||||
spyOn(console, 'error');
|
||||
|
||||
void <Component><Component /><Component /></Component>;
|
||||
|
||||
expectDev(console.error.calls.count()).toBe(0);
|
||||
ReactTestUtils.renderIntoDocument(
|
||||
<Component>
|
||||
<Component />
|
||||
<Component />
|
||||
</Component>,
|
||||
);
|
||||
});
|
||||
|
||||
it('does not warn when the child array contains non-elements', () => {
|
||||
spyOn(console, 'error');
|
||||
|
||||
void <Component>{[{}, {}]}</Component>;
|
||||
|
||||
expectDev(console.error.calls.count()).toBe(0);
|
||||
});
|
||||
|
||||
it('should give context for PropType errors in nested components.', () => {
|
||||
@@ -246,14 +313,12 @@ describe('ReactJSXElementValidator', () => {
|
||||
var Undefined = undefined;
|
||||
var Null = null;
|
||||
var True = true;
|
||||
var Num = 123;
|
||||
var Div = 'div';
|
||||
spyOn(console, 'error');
|
||||
void <Undefined />;
|
||||
void <Null />;
|
||||
void <True />;
|
||||
void <Num />;
|
||||
expectDev(console.error.calls.count()).toBe(4);
|
||||
expectDev(console.error.calls.count()).toBe(3);
|
||||
expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe(
|
||||
'Warning: React.createElement: type is invalid -- expected a string ' +
|
||||
'(for built-in components) or a class/function (for composite ' +
|
||||
@@ -273,14 +338,8 @@ describe('ReactJSXElementValidator', () => {
|
||||
'components) but got: boolean.' +
|
||||
'\n\nCheck your code at **.',
|
||||
);
|
||||
expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(3)[0])).toBe(
|
||||
'Warning: React.createElement: type is invalid -- expected a string ' +
|
||||
'(for built-in components) or a class/function (for composite ' +
|
||||
'components) but got: number.' +
|
||||
'\n\nCheck your code at **.',
|
||||
);
|
||||
void <Div />;
|
||||
expectDev(console.error.calls.count()).toBe(4);
|
||||
expectDev(console.error.calls.count()).toBe(3);
|
||||
});
|
||||
|
||||
it('should check default prop values', () => {
|
||||
|
||||
Reference in New Issue
Block a user