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:
Clement Hoang
2017-10-30 17:52:40 -07:00
committed by GitHub
parent d61af82390
commit 4ce5da7aee
10 changed files with 1120 additions and 149 deletions
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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.',
);
}
});
});
+7
View File
@@ -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,
+51 -2
View File
@@ -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', () => {