diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegration-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegration-test.js
index 06d8a0eef18c9..4d09d2e04a281 100644
--- a/packages/react-dom/src/__tests__/ReactDOMServerIntegration-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegration-test.js
@@ -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(text1
);
+ let parent = e.parentNode;
+ expect(parent.childNodes[0].tagName).toBe('DIV');
+ });
+
+ itRenders('a fragment with several children', async render => {
+ let Header = props => {
+ return
header
;
+ };
+ let Footer = props => {
+ return footer
about
;
+ };
+ let e = await render(
+
+ text1
+ text2
+
+
+ ,
+ );
+ 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(
+
+
+ text1
+
+ text2
+
+
+ {null}{false}
+
+
+ ,
+ );
+ 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({''}
)).textContent).toBe('');
+ expect(await render()).toBe(null);
expect(await render([])).toBe(null);
expect(await render(false)).toBe(null);
expect(await render(true)).toBe(null);
diff --git a/packages/react-dom/src/server/ReactPartialRenderer.js b/packages/react-dom/src/server/ReactPartialRenderer.js
index 62ea2e042824f..277eef8995943 100644
--- a/packages/react-dom/src/server/ReactPartialRenderer.js
+++ b/packages/react-dom/src/server/ReactPartialRenderer.js
@@ -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;
@@ -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);
}
}
}
diff --git a/packages/react-noop-renderer/src/ReactNoop.js b/packages/react-noop-renderer/src/ReactNoop.js
index 6624ce9f72b70..9ecbc80fd25df 100644
--- a/packages/react-noop-renderer/src/ReactNoop.js
+++ b/packages/react-noop-renderer/src/ReactNoop.js
@@ -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) {
diff --git a/packages/react-reconciler/src/ReactChildFiber.js b/packages/react-reconciler/src/ReactChildFiber.js
index 994ef74e6a278..8c736de5fd857 100644
--- a/packages/react-reconciler/src/ReactChildFiber.js
+++ b/packages/react-reconciler/src/ReactChildFiber.js
@@ -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(
diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js
index 1ee7867efdd07..079c50708e99f 100644
--- a/packages/react-reconciler/src/ReactFiber.js
+++ b/packages/react-reconciler/src/ReactFiber.js
@@ -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);
diff --git a/packages/react-reconciler/src/__tests__/ReactFragment-test.js b/packages/react-reconciler/src/__tests__/ReactFragment-test.js
new file mode 100644
index 0000000000000..2083d8643cfc8
--- /dev/null
+++ b/packages/react-reconciler/src/__tests__/ReactFragment-test.js
@@ -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 = (
+
+ foo
+
+ );
+
+ ReactNoop.render(element);
+ ReactNoop.flush();
+
+ expect(ReactNoop.getChildren()).toEqual([span()]);
+ });
+
+ it('should render zero children via noop renderer', () => {
+ const element = ;
+
+ ReactNoop.render(element);
+ ReactNoop.flush();
+
+ expect(ReactNoop.getChildren()).toEqual([]);
+ });
+
+ it('should render multiple children via noop renderer', () => {
+ const element = (
+
+ hello world
+
+ );
+
+ ReactNoop.render(element);
+ ReactNoop.flush();
+
+ expect(ReactNoop.getChildren()).toEqual([text('hello '), span()]);
+ });
+
+ it('should render an iterable via noop renderer', () => {
+ const element = (
+
+ {new Set([hi, bye])}
+
+ );
+
+ 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 Hello
;
+ }
+ }
+
+ function Foo({condition}) {
+ return condition
+ ?
+ :
+
+ World
+ ;
+ }
+
+ ReactNoop.render();
+ ReactNoop.flush();
+
+ ReactNoop.render();
+ ReactNoop.flush();
+
+ expect(ops).toEqual(['Update Stateful']);
+ expect(ReactNoop.getChildren()).toEqual([div(), div()]);
+
+ ReactNoop.render();
+ 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 Hello
;
+ }
+ }
+
+ function Foo({condition}) {
+ return condition
+ ?
+
+
+ :
+
+ ;
+ }
+
+ ReactNoop.render();
+ ReactNoop.flush();
+
+ ReactNoop.render();
+ ReactNoop.flush();
+
+ expect(ops).toEqual(['Update Stateful']);
+ expect(ReactNoop.getChildren()).toEqual([div()]);
+
+ ReactNoop.render();
+ 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 Hello
;
+ }
+ }
+
+ function Foo({condition}) {
+ return condition
+ ?
+
+
+
+
+
+
+ :
+
+
+
+
+
+
+ ;
+ }
+
+ ReactNoop.render();
+ ReactNoop.flush();
+
+ ReactNoop.render();
+ ReactNoop.flush();
+
+ expect(ops).toEqual(['Update Stateful']);
+ expect(ReactNoop.getChildren()).toEqual([div(), div()]);
+
+ ReactNoop.render();
+ 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 Hello
;
+ }
+ }
+
+ function Foo({condition}) {
+ return condition
+ ?
+
+
+ : ;
+ }
+
+ ReactNoop.render();
+ ReactNoop.flush();
+
+ ReactNoop.render();
+ ReactNoop.flush();
+
+ expect(ops).toEqual([]);
+ expect(ReactNoop.getChildren()).toEqual([div()]);
+
+ ReactNoop.render();
+ 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 Hello
;
+ }
+ }
+
+ function Foo({condition}) {
+ return condition
+ ?
+ :
+
+
+
+ ;
+ }
+
+ ReactNoop.render();
+ ReactNoop.flush();
+
+ ReactNoop.render();
+ ReactNoop.flush();
+
+ expect(ops).toEqual([]);
+ expect(ReactNoop.getChildren()).toEqual([div()]);
+
+ ReactNoop.render();
+ 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 Hello
;
+ }
+ }
+
+ function Foo({condition}) {
+ return condition
+ ?
+ :
+
+
+
+
+ ;
+ }
+
+ ReactNoop.render();
+ ReactNoop.flush();
+
+ ReactNoop.render();
+ ReactNoop.flush();
+
+ expect(ops).toEqual([]);
+ expect(ReactNoop.getChildren()).toEqual([div(), div()]);
+
+ ReactNoop.render();
+ 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 Hello
;
+ }
+ }
+
+ function Foo({condition}) {
+ return condition
+ ?
+
+
+ :
+ {[]}
+ ;
+ }
+
+ ReactNoop.render();
+ ReactNoop.flush();
+
+ ReactNoop.render();
+ ReactNoop.flush();
+
+ expect(ops).toEqual(['Update Stateful']);
+ expect(ReactNoop.getChildren()).toEqual([div()]);
+
+ ReactNoop.render();
+ 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 Hello
;
+ }
+ }
+
+ function Foo({condition}) {
+ return condition
+ ? []
+ :
+
+ ;
+ }
+
+ ReactNoop.render();
+ ReactNoop.flush();
+
+ ReactNoop.render();
+ ReactNoop.flush();
+
+ expect(ops).toEqual(['Update Stateful']);
+ expect(ReactNoop.getChildren()).toEqual([div()]);
+
+ ReactNoop.render();
+ 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 Hello
;
+ }
+ }
+
+ function Foo({condition}) {
+ return condition
+ ? {[]}
+ :
+
+ ;
+ }
+
+ ReactNoop.render();
+ ReactNoop.flush();
+
+ ReactNoop.render();
+ ReactNoop.flush();
+
+ expect(ops).toEqual([]);
+ expect(ReactNoop.getChildren()).toEqual([div()]);
+
+ ReactNoop.render();
+ 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 Hello
;
+ }
+ }
+
+ function Foo({condition}) {
+ return condition
+ ? {[]}
+ : [[]];
+ }
+
+ ReactNoop.render();
+ ReactNoop.flush();
+
+ ReactNoop.render();
+ ReactNoop.flush();
+
+ expect(ops).toEqual([]);
+ expect(ReactNoop.getChildren()).toEqual([div()]);
+
+ ReactNoop.render();
+ 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 Hello
;
+ }
+ }
+
+ function Foo({condition}) {
+ return condition
+ ?
+
+
+ : [[]];
+ }
+
+ ReactNoop.render();
+ ReactNoop.flush();
+
+ ReactNoop.render();
+ ReactNoop.flush();
+
+ expect(ops).toEqual(['Update Stateful']);
+ expect(ReactNoop.getChildren()).toEqual([div()]);
+
+ ReactNoop.render();
+ 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 Hello
;
+ }
+ }
+
+ function Foo({condition}) {
+ return condition
+ ?
+
+
+ :
+
+ World
+ ;
+ }
+
+ ReactNoop.render();
+ ReactNoop.flush();
+
+ ReactNoop.render();
+ ReactNoop.flush();
+
+ expect(ops).toEqual([]);
+ expect(ReactNoop.getChildren()).toEqual([div(), span()]);
+
+ ReactNoop.render();
+ 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 Hello
;
+ }
+ }
+
+ function Foo({condition}) {
+ return condition
+ ?
+
+
+ :
+
+ ;
+ }
+
+ ReactNoop.render();
+ ReactNoop.flush();
+
+ ReactNoop.render();
+ ReactNoop.flush();
+
+ expect(ops).toEqual([]);
+ expect(ReactNoop.getChildren()).toEqual([div()]);
+
+ ReactNoop.render();
+ 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 Hello
;
+ }
+ }
+
+ function Foo({condition}) {
+ return condition
+ ?
+ : ;
+ }
+
+ ReactNoop.render();
+ ReactNoop.flush();
+
+ ReactNoop.render();
+ ReactNoop.flush();
+
+ expect(ops).toEqual(['Update Stateful']);
+ expect(ReactNoop.getChildren()).toEqual([div(span(), div(div()), span())]);
+
+ ReactNoop.render();
+ 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 Hello
;
+ }
+ }
+
+ function Foo({condition}) {
+ return condition
+ ?
+ {}
+
+ : {[]}
;
+ }
+
+ ReactNoop.render();
+ ReactNoop.flush();
+
+ ReactNoop.render();
+ ReactNoop.flush();
+
+ expect(ops).toEqual([]);
+ expect(ReactNoop.getChildren()).toEqual([div(div(), span())]);
+
+ ReactNoop.render();
+ 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 Hello
;
+ }
+ }
+
+ function Foo({condition}) {
+ return condition
+ ? [, ]
+ : [, ];
+ }
+
+ ReactNoop.render();
+ ReactNoop.flush();
+
+ ReactNoop.render();
+ ReactNoop.flush();
+
+ expect(ops).toEqual(['Update Stateful']);
+ expect(ReactNoop.getChildren()).toEqual([span(), div()]);
+
+ ReactNoop.render();
+ 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.',
+ );
+ }
+ });
+});
diff --git a/packages/react/src/React.js b/packages/react/src/React.js
index fc718d8c9a5fd..eab0ced7f2344 100644
--- a/packages/react/src/React.js
+++ b/packages/react/src/React.js
@@ -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,
diff --git a/packages/react/src/ReactElementValidator.js b/packages/react/src/ReactElementValidator.js
index 94400faf2f6fb..a48137da6cf5f 100644
--- a/packages/react/src/ReactElementValidator.js
+++ b/packages/react/src/ReactElementValidator.js
@@ -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;
},
diff --git a/packages/react/src/__tests__/ReactElementValidator-test.js b/packages/react/src/__tests__/ReactElementValidator-test.js
index 273669258ce86..a376e061d0ea4 100644
--- a/packages/react/src/__tests__/ReactElementValidator-test.js
+++ b/packages/react/src/__tests__/ReactElementValidator-test.js
@@ -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', () => {
diff --git a/packages/react/src/__tests__/ReactJSXElementValidator-test.js b/packages/react/src/__tests__/ReactJSXElementValidator-test.js
index 8cf5ee66dd7f0..025df94dac7fc 100644
--- a/packages/react/src/__tests__/ReactJSXElementValidator-test.js
+++ b/packages/react/src/__tests__/ReactJSXElementValidator-test.js
@@ -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 {[, ]};
+ ReactTestUtils.renderIntoDocument(
+ {[, ]},
+ );
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 hello;
+ }
+ }
+
+ ReactTestUtils.renderIntoDocument();
+
+ 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 (
+ {
+ this.foo = bar;
+ }}>
+ hello
+
+ );
+ }
+ }
+
+ ReactTestUtils.renderIntoDocument();
+
+ 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 {iterable};
+ ReactTestUtils.renderIntoDocument({iterable});
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(
+
+ 1
+ 2
+ ,
+ );
+ });
+
+ it('warns for fragments of multiple elements with same key', () => {
spyOn(console, 'error');
- void (
- {[, ]}
+ ReactTestUtils.renderIntoDocument(
+
+ 1
+ 2
+ 3
+ ,
+ );
+
+ 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(
+ {[, ]},
);
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 {iterable};
+ ReactTestUtils.renderIntoDocument({iterable});
expectDev(console.error.calls.count()).toBe(0);
});
@@ -156,25 +226,22 @@ describe('ReactJSXElementValidator', () => {
};
iterable.entries = iterable['@@iterator'];
- void {iterable};
+ ReactTestUtils.renderIntoDocument({iterable});
expectDev(console.error.calls.count()).toBe(0);
});
it('does not warn when the element is directly as children', () => {
- spyOn(console, 'error');
-
- void ;
-
- expectDev(console.error.calls.count()).toBe(0);
+ ReactTestUtils.renderIntoDocument(
+
+
+
+ ,
+ );
});
it('does not warn when the child array contains non-elements', () => {
- spyOn(console, 'error');
-
void {[{}, {}]};
-
- 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 ;
void ;
void ;
- void ;
- 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 ;
- expectDev(console.error.calls.count()).toBe(4);
+ expectDev(console.error.calls.count()).toBe(3);
});
it('should check default prop values', () => {