@samitouri / QOS-React / commits / 4ce5da7aee

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

Clement Hoang committed Oct 30, 2017 at 17:52 UTC 4ce5da7aee90a373f2f36d1beb559097af30952e
10 files changed +1120 -149
packages/react-dom/src/__tests__/ReactDOMServerIntegration-test.js
+50
@@ -403,6 +403,55 @@ describe('ReactDOMServerIntegration', () => {
403 expect(parent.childNodes[2].tagName).toBe('P');
404 });
405
406 + itRenders('a fragment with one child', async render => {
407 + let e = await render(<React.Fragment><div>text1</div></React.Fragment>);
408 + let parent = e.parentNode;
409 + expect(parent.childNodes[0].tagName).toBe('DIV');
410 + });
411 +
412 + itRenders('a fragment with several children', async render => {
413 + let Header = props => {
414 + return <p>header</p>;
415 + };
416 + let Footer = props => {
417 + return <React.Fragment><h2>footer</h2><h3>about</h3></React.Fragment>;
418 + };
419 + let e = await render(
420 + <React.Fragment>
421 + <div>text1</div>
422 + <span>text2</span>
423 + <Header />
424 + <Footer />
425 + </React.Fragment>,
426 + );
427 + let parent = e.parentNode;
428 + expect(parent.childNodes[0].tagName).toBe('DIV');
429 + expect(parent.childNodes[1].tagName).toBe('SPAN');
430 + expect(parent.childNodes[2].tagName).toBe('P');
431 + expect(parent.childNodes[3].tagName).toBe('H2');
432 + expect(parent.childNodes[4].tagName).toBe('H3');
433 + });
434 +
435 + itRenders('a nested fragment', async render => {
436 + let e = await render(
437 + <React.Fragment>
438 + <React.Fragment>
439 + <div>text1</div>
440 + </React.Fragment>
441 + <span>text2</span>
442 + <React.Fragment>
443 + <React.Fragment>
444 + <React.Fragment>{null}<p /></React.Fragment>{false}
445 + </React.Fragment>
446 + </React.Fragment>
447 + </React.Fragment>,
448 + );
449 + let parent = e.parentNode;
450 + expect(parent.childNodes[0].tagName).toBe('DIV');
451 + expect(parent.childNodes[1].tagName).toBe('SPAN');
452 + expect(parent.childNodes[2].tagName).toBe('P');
453 + });
454 +
455 itRenders('an iterable', async render => {
456 const threeDivIterable = {
457 '@@iterator': function() {
@@ -435,6 +484,7 @@ describe('ReactDOMServerIntegration', () => {
484 // but server returns empty HTML. So we compare parent text.
485 expect((await render(<div>{''}</div>)).textContent).toBe('');
486
487 + expect(await render(<React.Fragment />)).toBe(null);
488 expect(await render([])).toBe(null);
489 expect(await render(false)).toBe(null);
490 expect(await render(true)).toBe(null);
packages/react-dom/src/server/ReactPartialRenderer.js
+59 -27
@@ -31,6 +31,12 @@ var escapeTextContentForBrowser = require('../shared/escapeTextContentForBrowser
31 var isCustomComponent = require('../shared/isCustomComponent');
32 var omittedCloseTags = require('../shared/omittedCloseTags');
33
34 +var REACT_FRAGMENT_TYPE =
35 + (typeof Symbol === 'function' &&
36 + Symbol.for &&
37 + Symbol.for('react.fragment')) ||
38 + 0xeacb;
39 +
40 // Based on reading the React.Children implementation. TODO: type this somewhere?
41 type ReactNode = string | number | ReactElement;
42 type FlatReactChildren = Array<null | ReactNode>;
@@ -206,6 +212,22 @@ function getNonChildrenInnerMarkup(props) {
212 return null;
213 }
214
215 +function flattenTopLevelChildren(children: mixed): FlatReactChildren {
216 + if (!React.isValidElement(children)) {
217 + return toArray(children);
218 + }
219 + const element = ((children: any): ReactElement);
220 + if (element.type !== REACT_FRAGMENT_TYPE) {
221 + return [element];
222 + }
223 + const fragmentChildren = element.props.children;
224 + if (!React.isValidElement(fragmentChildren)) {
225 + return toArray(fragmentChildren);
226 + }
227 + const fragmentChildElement = ((fragmentChildren: any): ReactElement);
228 + return [fragmentChildElement];
229 +}
230 +
231 function flattenOptionChildren(children: mixed): string {
232 var content = '';
233 // Flatten children and warn if they aren't strings or numbers;
@@ -482,14 +504,8 @@ class ReactDOMServerRenderer {
504 makeStaticMarkup: boolean;
505
506 constructor(children: mixed, makeStaticMarkup: boolean) {
485 - var flatChildren;
486 - if (React.isValidElement(children)) {
487 - // Safe because we just checked it's an element.
488 - var element = ((children: any): ReactElement);
489 - flatChildren = [element];
490 - } else {
491 - flatChildren = toArray(children);
492 - }
507 + const flatChildren = flattenTopLevelChildren(children);
508 +
509 var topFrame: Frame = {
510 // Assume all trees start in the HTML namespace (not totally true, but
511 // this is what we did historically)
@@ -569,26 +585,42 @@ class ReactDOMServerRenderer {
585 ({child: nextChild, context} = resolve(child, context));
586 if (nextChild === null || nextChild === false) {
587 return '';
572 - } else {
573 - if (React.isValidElement(nextChild)) {
574 - // Safe because we just checked it's an element.
575 - var nextElement = ((nextChild: any): ReactElement);
576 - return this.renderDOM(nextElement, context, parentNamespace);
577 - } else {
578 - var nextChildren = toArray(nextChild);
579 - var frame: Frame = {
580 - domNamespace: parentNamespace,
581 - children: nextChildren,
582 - childIndex: 0,
583 - context: context,
584 - footer: '',
585 - };
586 - if (__DEV__) {
587 - ((frame: any): FrameDev).debugElementStack = [];
588 - }
589 - this.stack.push(frame);
590 - return '';
588 + } else if (!React.isValidElement(nextChild)) {
589 + const nextChildren = toArray(nextChild);
590 + const frame: Frame = {
591 + domNamespace: parentNamespace,
592 + children: nextChildren,
593 + childIndex: 0,
594 + context: context,
595 + footer: '',
596 + };
597 + if (__DEV__) {
598 + ((frame: any): FrameDev).debugElementStack = [];
599 }
600 + this.stack.push(frame);
601 + return '';
602 + } else if (
603 + ((nextChild: any): ReactElement).type === REACT_FRAGMENT_TYPE
604 + ) {
605 + const nextChildren = toArray(
606 + ((nextChild: any): ReactElement).props.children,
607 + );
608 + const frame: Frame = {
609 + domNamespace: parentNamespace,
610 + children: nextChildren,
611 + childIndex: 0,
612 + context: context,
613 + footer: '',
614 + };
615 + if (__DEV__) {
616 + ((frame: any): FrameDev).debugElementStack = [];
617 + }
618 + this.stack.push(frame);
619 + return '';
620 + } else {
621 + // Safe because we just checked it's an element.
622 + var nextElement = ((nextChild: any): ReactElement);
623 + return this.renderDOM(nextElement, context, parentNamespace);
624 }
625 }
626 }
packages/react-noop-renderer/src/ReactNoop.js
+2 -1
@@ -512,7 +512,8 @@ var ReactNoop = {
512 log(
513 ' '.repeat(depth) +
514 '- ' +
515 - (fiber.type ? fiber.type.name || fiber.type : '[root]'),
515 + // need to explicitly coerce Symbol to a string
516 + (fiber.type ? fiber.type.name || fiber.type.toString() : '[root]'),
517 '[' + fiber.expirationTime + (fiber.pendingProps ? '*' : '') + ']',
518 );
519 if (fiber.updateQueue) {
packages/react-reconciler/src/ReactChildFiber.js
+103 -33
@@ -103,14 +103,17 @@ const FAUX_ITERATOR_SYMBOL = '@@iterator'; // Before Symbol spec.
103 var REACT_ELEMENT_TYPE;
104 var REACT_CALL_TYPE;
105 var REACT_RETURN_TYPE;
106 +var REACT_FRAGMENT_TYPE;
107 if (typeof Symbol === 'function' && Symbol.for) {
108 REACT_ELEMENT_TYPE = Symbol.for('react.element');
109 REACT_CALL_TYPE = Symbol.for('react.call');
110 REACT_RETURN_TYPE = Symbol.for('react.return');
111 + REACT_FRAGMENT_TYPE = Symbol.for('react.fragment');
112 } else {
113 REACT_ELEMENT_TYPE = 0xeac7;
114 REACT_CALL_TYPE = 0xeac8;
115 REACT_RETURN_TYPE = 0xeac9;
116 + REACT_FRAGMENT_TYPE = 0xeacb;
117 }
118
119 function getIteratorFn(maybeIterable: ?any): ?() => ?Iterator<*> {
@@ -385,17 +388,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
388 element: ReactElement,
389 expirationTime: ExpirationTime,
390 ): Fiber {
388 - if (current === null || current.type !== element.type) {
389 - // Insert
390 - const created = createFiberFromElement(
391 - element,
392 - returnFiber.internalContextTag,
393 - expirationTime,
394 - );
395 - created.ref = coerceRef(current, element);
396 - created.return = returnFiber;
397 - return created;
398 - } else {
391 + if (current !== null && current.type === element.type) {
392 // Move based on index
393 const existing = useFiber(current, expirationTime);
394 existing.ref = coerceRef(current, element);
@@ -406,6 +399,16 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
399 existing._debugOwner = element._owner;
400 }
401 return existing;
402 + } else {
403 + // Insert
404 + const created = createFiberFromElement(
405 + element,
406 + returnFiber.internalContextTag,
407 + expirationTime,
408 + );
409 + created.ref = coerceRef(current, element);
410 + created.return = returnFiber;
411 + return created;
412 }
413 }
414
@@ -493,6 +496,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
496 current: Fiber | null,
497 fragment: Iterable<*>,
498 expirationTime: ExpirationTime,
499 + key: null | string,
500 ): Fiber {
501 if (current === null || current.tag !== Fragment) {
502 // Insert
@@ -500,6 +504,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
504 fragment,
505 returnFiber.internalContextTag,
506 expirationTime,
507 + key,
508 );
509 created.return = returnFiber;
510 return created;
@@ -533,14 +538,25 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
538 if (typeof newChild === 'object' && newChild !== null) {
539 switch (newChild.$$typeof) {
540 case REACT_ELEMENT_TYPE: {
536 - const created = createFiberFromElement(
537 - newChild,
538 - returnFiber.internalContextTag,
539 - expirationTime,
540 - );
541 - created.ref = coerceRef(null, newChild);
542 - created.return = returnFiber;
543 - return created;
541 + if (newChild.type === REACT_FRAGMENT_TYPE) {
542 + const created = createFiberFromFragment(
543 + newChild.props.children,
544 + returnFiber.internalContextTag,
545 + expirationTime,
546 + newChild.key,
547 + );
548 + created.return = returnFiber;
549 + return created;
550 + } else {
551 + const created = createFiberFromElement(
552 + newChild,
553 + returnFiber.internalContextTag,
554 + expirationTime,
555 + );
556 + created.ref = coerceRef(null, newChild);
557 + created.return = returnFiber;
558 + return created;
559 + }
560 }
561
562 case REACT_CALL_TYPE: {
@@ -580,6 +596,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
596 newChild,
597 returnFiber.internalContextTag,
598 expirationTime,
599 + null,
600 );
601 created.return = returnFiber;
602 return created;
@@ -626,6 +643,15 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
643 switch (newChild.$$typeof) {
644 case REACT_ELEMENT_TYPE: {
645 if (newChild.key === key) {
646 + if (newChild.type === REACT_FRAGMENT_TYPE) {
647 + return updateFragment(
648 + returnFiber,
649 + oldFiber,
650 + newChild.props.children,
651 + expirationTime,
652 + key,
653 + );
654 + }
655 return updateElement(
656 returnFiber,
657 oldFiber,
@@ -676,12 +702,17 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
702 }
703
704 if (isArray(newChild) || getIteratorFn(newChild)) {
679 - // Fragments don't have keys so if the previous key is implicit we can
680 - // update it.
705 if (key !== null) {
706 return null;
707 }
684 - return updateFragment(returnFiber, oldFiber, newChild, expirationTime);
708 +
709 + return updateFragment(
710 + returnFiber,
711 + oldFiber,
712 + newChild,
713 + expirationTime,
714 + null,
715 + );
716 }
717
718 throwOnInvalidObjectType(returnFiber, newChild);
@@ -722,6 +753,15 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
753 existingChildren.get(
754 newChild.key === null ? newIdx : newChild.key,
755 ) || null;
756 + if (newChild.type === REACT_FRAGMENT_TYPE) {
757 + return updateFragment(
758 + returnFiber,
759 + matchedFiber,
760 + newChild.props.children,
761 + expirationTime,
762 + newChild.key,
763 + );
764 + }
765 return updateElement(
766 returnFiber,
767 matchedFiber,
@@ -776,6 +816,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
816 matchedFiber,
817 newChild,
818 expirationTime,
819 + null,
820 );
821 }
822
@@ -1213,11 +1254,17 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
1254 // TODO: If key === null and child.key === null, then this only applies to
1255 // the first item in the list.
1256 if (child.key === key) {
1216 - if (child.type === element.type) {
1257 + if (
1258 + child.tag === Fragment
1259 + ? element.type === REACT_FRAGMENT_TYPE
1260 + : child.type === element.type
1261 + ) {
1262 deleteRemainingChildren(returnFiber, child.sibling);
1263 const existing = useFiber(child, expirationTime);
1264 existing.ref = coerceRef(child, element);
1220 - existing.pendingProps = element.props;
1265 + existing.pendingProps = element.type === REACT_FRAGMENT_TYPE
1266 + ? element.props.children
1267 + : element.props;
1268 existing.return = returnFiber;
1269 if (__DEV__) {
1270 existing._debugSource = element._source;
@@ -1234,14 +1281,25 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
1281 child = child.sibling;
1282 }
1283
1237 - const created = createFiberFromElement(
1238 - element,
1239 - returnFiber.internalContextTag,
1240 - expirationTime,
1241 - );
1242 - created.ref = coerceRef(currentFirstChild, element);
1243 - created.return = returnFiber;
1244 - return created;
1284 + if (element.type === REACT_FRAGMENT_TYPE) {
1285 + const created = createFiberFromFragment(
1286 + element.props.children,
1287 + returnFiber.internalContextTag,
1288 + expirationTime,
1289 + element.key,
1290 + );
1291 + created.return = returnFiber;
1292 + return created;
1293 + } else {
1294 + const created = createFiberFromElement(
1295 + element,
1296 + returnFiber.internalContextTag,
1297 + expirationTime,
1298 + );
1299 + created.ref = coerceRef(currentFirstChild, element);
1300 + created.return = returnFiber;
1301 + return created;
1302 + }
1303 }
1304
1305 function reconcileSingleCall(
@@ -1366,8 +1424,21 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
1424 // not as a fragment. Nested arrays on the other hand will be treated as
1425 // fragment nodes. Recursion happens at the normal flow.
1426
1427 + // Handle top level unkeyed fragments as if they were arrays.
1428 + // This leads to an ambiguity between <>{[...]}</> and <>...</>.
1429 + // We treat the ambiguous cases above the same.
1430 + if (
1431 + typeof newChild === 'object' &&
1432 + newChild !== null &&
1433 + newChild.type === REACT_FRAGMENT_TYPE &&
1434 + newChild.key === null
1435 + ) {
1436 + newChild = newChild.props.children;
1437 + }
1438 +
1439 // Handle object types
1440 const isObject = typeof newChild === 'object' && newChild !== null;
1441 +
1442 if (isObject) {
1443 switch (newChild.$$typeof) {
1444 case REACT_ELEMENT_TYPE:
@@ -1398,7 +1469,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
1469 expirationTime,
1470 ),
1471 );
1401 -
1472 case REACT_PORTAL_TYPE:
1473 return placeSingleChild(
1474 reconcileSinglePortal(
packages/react-reconciler/src/ReactFiber.js
+37 -49
@@ -301,62 +301,18 @@ exports.createFiberFromElement = function(
301 owner = element._owner;
302 }
303
304 - const fiber = createFiberFromElementType(
305 - element.type,
306 - element.key,
307 - internalContextTag,
308 - owner,
309 - );
310 - fiber.pendingProps = element.props;
311 - fiber.expirationTime = expirationTime;
312 -
313 - if (__DEV__) {
314 - fiber._debugSource = element._source;
315 - fiber._debugOwner = element._owner;
316 - }
317 -
318 - return fiber;
319 -};
320 -
321 -exports.createFiberFromFragment = function(
322 - elements: ReactFragment,
323 - internalContextTag: TypeOfInternalContext,
324 - expirationTime: ExpirationTime,
325 -): Fiber {
326 - // TODO: Consider supporting keyed fragments. Technically, we accidentally
327 - // support that in the existing React.
328 - const fiber = createFiber(Fragment, null, internalContextTag);
329 - fiber.pendingProps = elements;
330 - fiber.expirationTime = expirationTime;
331 - return fiber;
332 -};
333 -
334 -exports.createFiberFromText = function(
335 - content: string,
336 - internalContextTag: TypeOfInternalContext,
337 - expirationTime: ExpirationTime,
338 -): Fiber {
339 - const fiber = createFiber(HostText, null, internalContextTag);
340 - fiber.pendingProps = content;
341 - fiber.expirationTime = expirationTime;
342 - return fiber;
343 -};
344 -
345 -function createFiberFromElementType(
346 - type: mixed,
347 - key: null | string,
348 - internalContextTag: TypeOfInternalContext,
349 - debugOwner: null | Fiber,
350 -): Fiber {
304 let fiber;
305 + const {type, key} = element;
306 if (typeof type === 'function') {
307 fiber = shouldConstruct(type)
308 ? createFiber(ClassComponent, key, internalContextTag)
309 : createFiber(IndeterminateComponent, key, internalContextTag);
310 fiber.type = type;
311 + fiber.pendingProps = element.props;
312 } else if (typeof type === 'string') {
313 fiber = createFiber(HostComponent, key, internalContextTag);
314 fiber.type = type;
315 + fiber.pendingProps = element.props;
316 } else if (
317 typeof type === 'object' &&
318 type !== null &&
@@ -369,6 +325,7 @@ function createFiberFromElementType(
325 // we don't know if we can reuse that fiber or if we need to clone it.
326 // There is probably a clever way to restructure this.
327 fiber = ((type: any): Fiber);
328 + fiber.pendingProps = element.props;
329 } else {
330 let info = '';
331 if (__DEV__) {
@@ -382,7 +339,7 @@ function createFiberFromElementType(
339 ' You likely forgot to export your component from the file ' +
340 "it's defined in.";
341 }
385 - const ownerName = debugOwner ? getComponentName(debugOwner) : null;
342 + const ownerName = owner ? getComponentName(owner) : null;
343 if (ownerName) {
344 info += '\n\nCheck the render method of `' + ownerName + '`.';
345 }
@@ -395,10 +352,41 @@ function createFiberFromElementType(
352 info,
353 );
354 }
355 +
356 + if (__DEV__) {
357 + fiber._debugSource = element._source;
358 + fiber._debugOwner = element._owner;
359 + }
360 +
361 + fiber.expirationTime = expirationTime;
362 +
363 + return fiber;
364 +};
365 +
366 +function createFiberFromFragment(
367 + elements: ReactFragment,
368 + internalContextTag: TypeOfInternalContext,
369 + expirationTime: ExpirationTime,
370 + key: null | string,
371 +): Fiber {
372 + const fiber = createFiber(Fragment, key, internalContextTag);
373 + fiber.pendingProps = elements;
374 + fiber.expirationTime = expirationTime;
375 return fiber;
376 }
377
401 -exports.createFiberFromElementType = createFiberFromElementType;
378 +exports.createFiberFromFragment = createFiberFromFragment;
379 +
380 +exports.createFiberFromText = function(
381 + content: string,
382 + internalContextTag: TypeOfInternalContext,
383 + expirationTime: ExpirationTime,
384 +): Fiber {
385 + const fiber = createFiber(HostText, null, internalContextTag);
386 + fiber.pendingProps = content;
387 + fiber.expirationTime = expirationTime;
388 + return fiber;
389 +};
390
391 exports.createFiberFromHostInstanceForDeletion = function(): Fiber {
392 const fiber = createFiber(HostComponent, null, NoContext);
packages/react-reconciler/src/__tests__/ReactFragment-test.js new
+721
@@ -0,0 +1,721 @@
1 +/**
2 + * Copyright (c) 2013-present, Facebook, Inc.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @emails react-core
8 + */
9 +'use strict';
10 +
11 +let React;
12 +let ReactNoop;
13 +
14 +describe('ReactFragment', () => {
15 + beforeEach(function() {
16 + jest.resetModules();
17 + React = require('react');
18 + ReactNoop = require('react-noop-renderer');
19 + });
20 +
21 + function span(prop) {
22 + return {type: 'span', children: [], prop};
23 + }
24 +
25 + function text(val) {
26 + return {text: val};
27 + }
28 +
29 + function div(...children) {
30 + children = children.map(c => (typeof c === 'string' ? {text: c} : c));
31 + return {type: 'div', children, prop: undefined};
32 + }
33 +
34 + it('should render a single child via noop renderer', () => {
35 + const element = (
36 + <React.Fragment>
37 + <span>foo</span>
38 + </React.Fragment>
39 + );
40 +
41 + ReactNoop.render(element);
42 + ReactNoop.flush();
43 +
44 + expect(ReactNoop.getChildren()).toEqual([span()]);
45 + });
46 +
47 + it('should render zero children via noop renderer', () => {
48 + const element = <React.Fragment />;
49 +
50 + ReactNoop.render(element);
51 + ReactNoop.flush();
52 +
53 + expect(ReactNoop.getChildren()).toEqual([]);
54 + });
55 +
56 + it('should render multiple children via noop renderer', () => {
57 + const element = (
58 + <React.Fragment>
59 + hello <span>world</span>
60 + </React.Fragment>
61 + );
62 +
63 + ReactNoop.render(element);
64 + ReactNoop.flush();
65 +
66 + expect(ReactNoop.getChildren()).toEqual([text('hello '), span()]);
67 + });
68 +
69 + it('should render an iterable via noop renderer', () => {
70 + const element = (
71 + <React.Fragment>
72 + {new Set([<span key="a">hi</span>, <span key="b">bye</span>])}
73 + </React.Fragment>
74 + );
75 +
76 + ReactNoop.render(element);
77 + ReactNoop.flush();
78 +
79 + expect(ReactNoop.getChildren()).toEqual([span(), span()]);
80 + });
81 +
82 + it('should preserve state of children with 1 level nesting', function() {
83 + var ops = [];
84 +
85 + class Stateful extends React.Component {
86 + componentDidUpdate() {
87 + ops.push('Update Stateful');
88 + }
89 +
90 + render() {
91 + return <div>Hello</div>;
92 + }
93 + }
94 +
95 + function Foo({condition}) {
96 + return condition
97 + ? <Stateful key="a" />
98 + : <React.Fragment>
99 + <Stateful key="a" />
100 + <div key="b">World</div>
101 + </React.Fragment>;
102 + }
103 +
104 + ReactNoop.render(<Foo condition={true} />);
105 + ReactNoop.flush();
106 +
107 + ReactNoop.render(<Foo condition={false} />);
108 + ReactNoop.flush();
109 +
110 + expect(ops).toEqual(['Update Stateful']);
111 + expect(ReactNoop.getChildren()).toEqual([div(), div()]);
112 +
113 + ReactNoop.render(<Foo condition={true} />);
114 + ReactNoop.flush();
115 +
116 + expect(ops).toEqual(['Update Stateful', 'Update Stateful']);
117 + expect(ReactNoop.getChildren()).toEqual([div()]);
118 + });
119 +
120 + it('should preserve state between top-level fragments', function() {
121 + var ops = [];
122 +
123 + class Stateful extends React.Component {
124 + componentDidUpdate() {
125 + ops.push('Update Stateful');
126 + }
127 +
128 + render() {
129 + return <div>Hello</div>;
130 + }
131 + }
132 +
133 + function Foo({condition}) {
134 + return condition
135 + ? <React.Fragment>
136 + <Stateful />
137 + </React.Fragment>
138 + : <React.Fragment>
139 + <Stateful />
140 + </React.Fragment>;
141 + }
142 +
143 + ReactNoop.render(<Foo condition={true} />);
144 + ReactNoop.flush();
145 +
146 + ReactNoop.render(<Foo condition={false} />);
147 + ReactNoop.flush();
148 +
149 + expect(ops).toEqual(['Update Stateful']);
150 + expect(ReactNoop.getChildren()).toEqual([div()]);
151 +
152 + ReactNoop.render(<Foo condition={true} />);
153 + ReactNoop.flush();
154 +
155 + expect(ops).toEqual(['Update Stateful', 'Update Stateful']);
156 + expect(ReactNoop.getChildren()).toEqual([div()]);
157 + });
158 +
159 + it('should preserve state of children nested at same level', function() {
160 + var ops = [];
161 +
162 + class Stateful extends React.Component {
163 + componentDidUpdate() {
164 + ops.push('Update Stateful');
165 + }
166 +
167 + render() {
168 + return <div>Hello</div>;
169 + }
170 + }
171 +
172 + function Foo({condition}) {
173 + return condition
174 + ? <React.Fragment>
175 + <React.Fragment>
176 + <React.Fragment>
177 + <Stateful key="a" />
178 + </React.Fragment>
179 + </React.Fragment>
180 + </React.Fragment>
181 + : <React.Fragment>
182 + <React.Fragment>
183 + <React.Fragment>
184 + <div />
185 + <Stateful key="a" />
186 + </React.Fragment>
187 + </React.Fragment>
188 + </React.Fragment>;
189 + }
190 +
191 + ReactNoop.render(<Foo condition={true} />);
192 + ReactNoop.flush();
193 +
194 + ReactNoop.render(<Foo condition={false} />);
195 + ReactNoop.flush();
196 +
197 + expect(ops).toEqual(['Update Stateful']);
198 + expect(ReactNoop.getChildren()).toEqual([div(), div()]);
199 +
200 + ReactNoop.render(<Foo condition={true} />);
201 + ReactNoop.flush();
202 +
203 + expect(ops).toEqual(['Update Stateful', 'Update Stateful']);
204 + expect(ReactNoop.getChildren()).toEqual([div()]);
205 + });
206 +
207 + it('should not preserve state in non-top-level fragment nesting', function() {
208 + var ops = [];
209 +
210 + class Stateful extends React.Component {
211 + componentDidUpdate() {
212 + ops.push('Update Stateful');
213 + }
214 +
215 + render() {
216 + return <div>Hello</div>;
217 + }
218 + }
219 +
220 + function Foo({condition}) {
221 + return condition
222 + ? <React.Fragment>
223 + <React.Fragment><Stateful key="a" /></React.Fragment>
224 + </React.Fragment>
225 + : <React.Fragment><Stateful key="a" /></React.Fragment>;
226 + }
227 +
228 + ReactNoop.render(<Foo condition={true} />);
229 + ReactNoop.flush();
230 +
231 + ReactNoop.render(<Foo condition={false} />);
232 + ReactNoop.flush();
233 +
234 + expect(ops).toEqual([]);
235 + expect(ReactNoop.getChildren()).toEqual([div()]);
236 +
237 + ReactNoop.render(<Foo condition={true} />);
238 + ReactNoop.flush();
239 +
240 + expect(ops).toEqual([]);
241 + expect(ReactNoop.getChildren()).toEqual([div()]);
242 + });
243 +
244 + it('should not preserve state of children if nested 2 levels without siblings', function() {
245 + var ops = [];
246 +
247 + class Stateful extends React.Component {
248 + componentDidUpdate() {
249 + ops.push('Update Stateful');
250 + }
251 +
252 + render() {
253 + return <div>Hello</div>;
254 + }
255 + }
256 +
257 + function Foo({condition}) {
258 + return condition
259 + ? <Stateful key="a" />
260 + : <React.Fragment>
261 + <React.Fragment>
262 + <Stateful key="a" />
263 + </React.Fragment>
264 + </React.Fragment>;
265 + }
266 +
267 + ReactNoop.render(<Foo condition={true} />);
268 + ReactNoop.flush();
269 +
270 + ReactNoop.render(<Foo condition={false} />);
271 + ReactNoop.flush();
272 +
273 + expect(ops).toEqual([]);
274 + expect(ReactNoop.getChildren()).toEqual([div()]);
275 +
276 + ReactNoop.render(<Foo condition={true} />);
277 + ReactNoop.flush();
278 +
279 + expect(ops).toEqual([]);
280 + expect(ReactNoop.getChildren()).toEqual([div()]);
281 + });
282 +
283 + it('should not preserve state of children if nested 2 levels with siblings', function() {
284 + var ops = [];
285 +
286 + class Stateful extends React.Component {
287 + componentDidUpdate() {
288 + ops.push('Update Stateful');
289 + }
290 +
291 + render() {
292 + return <div>Hello</div>;
293 + }
294 + }
295 +
296 + function Foo({condition}) {
297 + return condition
298 + ? <Stateful key="a" />
299 + : <React.Fragment>
300 + <React.Fragment>
301 + <Stateful key="a" />
302 + </React.Fragment>
303 + <div />
304 + </React.Fragment>;
305 + }
306 +
307 + ReactNoop.render(<Foo condition={true} />);
308 + ReactNoop.flush();
309 +
310 + ReactNoop.render(<Foo condition={false} />);
311 + ReactNoop.flush();
312 +
313 + expect(ops).toEqual([]);
314 + expect(ReactNoop.getChildren()).toEqual([div(), div()]);
315 +
316 + ReactNoop.render(<Foo condition={true} />);
317 + ReactNoop.flush();
318 +
319 + expect(ops).toEqual([]);
320 + expect(ReactNoop.getChildren()).toEqual([div()]);
321 + });
322 +
323 + it('should preserve state between array nested in fragment and fragment', function() {
324 + var ops = [];
325 +
326 + class Stateful extends React.Component {
327 + componentDidUpdate() {
328 + ops.push('Update Stateful');
329 + }
330 +
331 + render() {
332 + return <div>Hello</div>;
333 + }
334 + }
335 +
336 + function Foo({condition}) {
337 + return condition
338 + ? <React.Fragment>
339 + <Stateful key="a" />
340 + </React.Fragment>
341 + : <React.Fragment>
342 + {[<Stateful key="a" />]}
343 + </React.Fragment>;
344 + }
345 +
346 + ReactNoop.render(<Foo condition={true} />);
347 + ReactNoop.flush();
348 +
349 + ReactNoop.render(<Foo condition={false} />);
350 + ReactNoop.flush();
351 +
352 + expect(ops).toEqual(['Update Stateful']);
353 + expect(ReactNoop.getChildren()).toEqual([div()]);
354 +
355 + ReactNoop.render(<Foo condition={true} />);
356 + ReactNoop.flush();
357 +
358 + expect(ops).toEqual(['Update Stateful', 'Update Stateful']);
359 + expect(ReactNoop.getChildren()).toEqual([div()]);
360 + });
361 +
362 + it('should preserve state between top level fragment and array', function() {
363 + var ops = [];
364 +
365 + class Stateful extends React.Component {
366 + componentDidUpdate() {
367 + ops.push('Update Stateful');
368 + }
369 +
370 + render() {
371 + return <div>Hello</div>;
372 + }
373 + }
374 +
375 + function Foo({condition}) {
376 + return condition
377 + ? [<Stateful key="a" />]
378 + : <React.Fragment>
379 + <Stateful key="a" />
380 + </React.Fragment>;
381 + }
382 +
383 + ReactNoop.render(<Foo condition={true} />);
384 + ReactNoop.flush();
385 +
386 + ReactNoop.render(<Foo condition={false} />);
387 + ReactNoop.flush();
388 +
389 + expect(ops).toEqual(['Update Stateful']);
390 + expect(ReactNoop.getChildren()).toEqual([div()]);
391 +
392 + ReactNoop.render(<Foo condition={true} />);
393 + ReactNoop.flush();
394 +
395 + expect(ops).toEqual(['Update Stateful', 'Update Stateful']);
396 + expect(ReactNoop.getChildren()).toEqual([div()]);
397 + });
398 +
399 + it('should not preserve state between array nested in fragment and double nested fragment', function() {
400 + var ops = [];
401 +
402 + class Stateful extends React.Component {
403 + componentDidUpdate() {
404 + ops.push('Update Stateful');
405 + }
406 +
407 + render() {
408 + return <div>Hello</div>;
409 + }
410 + }
411 +
412 + function Foo({condition}) {
413 + return condition
414 + ? <React.Fragment>{[<Stateful key="a" />]}</React.Fragment>
415 + : <React.Fragment>
416 + <React.Fragment><Stateful key="a" /></React.Fragment>
417 + </React.Fragment>;
418 + }
419 +
420 + ReactNoop.render(<Foo condition={true} />);
421 + ReactNoop.flush();
422 +
423 + ReactNoop.render(<Foo condition={false} />);
424 + ReactNoop.flush();
425 +
426 + expect(ops).toEqual([]);
427 + expect(ReactNoop.getChildren()).toEqual([div()]);
428 +
429 + ReactNoop.render(<Foo condition={true} />);
430 + ReactNoop.flush();
431 +
432 + expect(ops).toEqual([]);
433 + expect(ReactNoop.getChildren()).toEqual([div()]);
434 + });
435 +
436 + it('should not preserve state between array nested in fragment and double nested array', function() {
437 + var ops = [];
438 +
439 + class Stateful extends React.Component {
440 + componentDidUpdate() {
441 + ops.push('Update Stateful');
442 + }
443 +
444 + render() {
445 + return <div>Hello</div>;
446 + }
447 + }
448 +
449 + function Foo({condition}) {
450 + return condition
451 + ? <React.Fragment>{[<Stateful key="a" />]}</React.Fragment>
452 + : [[<Stateful key="a" />]];
453 + }
454 +
455 + ReactNoop.render(<Foo condition={true} />);
456 + ReactNoop.flush();
457 +
458 + ReactNoop.render(<Foo condition={false} />);
459 + ReactNoop.flush();
460 +
461 + expect(ops).toEqual([]);
462 + expect(ReactNoop.getChildren()).toEqual([div()]);
463 +
464 + ReactNoop.render(<Foo condition={true} />);
465 + ReactNoop.flush();
466 +
467 + expect(ops).toEqual([]);
468 + expect(ReactNoop.getChildren()).toEqual([div()]);
469 + });
470 +
471 + it('should preserve state between double nested fragment and double nested array', function() {
472 + var ops = [];
473 +
474 + class Stateful extends React.Component {
475 + componentDidUpdate() {
476 + ops.push('Update Stateful');
477 + }
478 +
479 + render() {
480 + return <div>Hello</div>;
481 + }
482 + }
483 +
484 + function Foo({condition}) {
485 + return condition
486 + ? <React.Fragment>
487 + <React.Fragment><Stateful key="a" /></React.Fragment>
488 + </React.Fragment>
489 + : [[<Stateful key="a" />]];
490 + }
491 +
492 + ReactNoop.render(<Foo condition={true} />);
493 + ReactNoop.flush();
494 +
495 + ReactNoop.render(<Foo condition={false} />);
496 + ReactNoop.flush();
497 +
498 + expect(ops).toEqual(['Update Stateful']);
499 + expect(ReactNoop.getChildren()).toEqual([div()]);
500 +
501 + ReactNoop.render(<Foo condition={true} />);
502 + ReactNoop.flush();
503 +
504 + expect(ops).toEqual(['Update Stateful', 'Update Stateful']);
505 + expect(ReactNoop.getChildren()).toEqual([div()]);
506 + });
507 +
508 + it('should not preserve state of children when the keys are different', function() {
509 + var ops = [];
510 +
511 + class Stateful extends React.Component {
512 + componentDidUpdate() {
513 + ops.push('Update Stateful');
514 + }
515 +
516 + render() {
517 + return <div>Hello</div>;
518 + }
519 + }
520 +
521 + function Foo({condition}) {
522 + return condition
523 + ? <React.Fragment key="a">
524 + <Stateful />
525 + </React.Fragment>
526 + : <React.Fragment key="b">
527 + <Stateful />
528 + <span>World</span>
529 + </React.Fragment>;
530 + }
531 +
532 + ReactNoop.render(<Foo condition={true} />);
533 + ReactNoop.flush();
534 +
535 + ReactNoop.render(<Foo condition={false} />);
536 + ReactNoop.flush();
537 +
538 + expect(ops).toEqual([]);
539 + expect(ReactNoop.getChildren()).toEqual([div(), span()]);
540 +
541 + ReactNoop.render(<Foo condition={true} />);
542 + ReactNoop.flush();
543 +
544 + expect(ops).toEqual([]);
545 + expect(ReactNoop.getChildren()).toEqual([div()]);
546 + });
547 +
548 + it('should not preserve state between unkeyed and keyed fragment', function() {
549 + var ops = [];
550 +
551 + class Stateful extends React.Component {
552 + componentDidUpdate() {
553 + ops.push('Update Stateful');
554 + }
555 +
556 + render() {
557 + return <div>Hello</div>;
558 + }
559 + }
560 +
561 + function Foo({condition}) {
562 + return condition
563 + ? <React.Fragment key="a">
564 + <Stateful />
565 + </React.Fragment>
566 + : <React.Fragment>
567 + <Stateful />
568 + </React.Fragment>;
569 + }
570 +
571 + ReactNoop.render(<Foo condition={true} />);
572 + ReactNoop.flush();
573 +
574 + ReactNoop.render(<Foo condition={false} />);
575 + ReactNoop.flush();
576 +
577 + expect(ops).toEqual([]);
578 + expect(ReactNoop.getChildren()).toEqual([div()]);
579 +
580 + ReactNoop.render(<Foo condition={true} />);
581 + ReactNoop.flush();
582 +
583 + expect(ops).toEqual([]);
584 + expect(ReactNoop.getChildren()).toEqual([div()]);
585 + });
586 +
587 + it('should preserve state with reordering in multiple levels', function() {
588 + var ops = [];
589 +
590 + class Stateful extends React.Component {
591 + componentDidUpdate() {
592 + ops.push('Update Stateful');
593 + }
594 +
595 + render() {
596 + return <div>Hello</div>;
597 + }
598 + }
599 +
600 + function Foo({condition}) {
601 + return condition
602 + ? <div>
603 + <React.Fragment key="c">
604 + <span>foo</span>
605 + <div key="b">
606 + <Stateful key="a" />
607 + </div>
608 + </React.Fragment>
609 + <span>boop</span>
610 + </div>
611 + : <div>
612 + <span>beep</span>
613 + <React.Fragment key="c">
614 + <div key="b">
615 + <Stateful key="a" />
616 + </div>
617 + <span>bar</span>
618 + </React.Fragment>
619 + </div>;
620 + }
621 +
622 + ReactNoop.render(<Foo condition={true} />);
623 + ReactNoop.flush();
624 +
625 + ReactNoop.render(<Foo condition={false} />);
626 + ReactNoop.flush();
627 +
628 + expect(ops).toEqual(['Update Stateful']);
629 + expect(ReactNoop.getChildren()).toEqual([div(span(), div(div()), span())]);
630 +
631 + ReactNoop.render(<Foo condition={true} />);
632 + ReactNoop.flush();
633 +
634 + expect(ops).toEqual(['Update Stateful', 'Update Stateful']);
635 + expect(ReactNoop.getChildren()).toEqual([div(span(), div(div()), span())]);
636 + });
637 +
638 + it('should not preserve state when switching to a keyed fragment to an array', function() {
639 + spyOn(console, 'error');
640 + var ops = [];
641 +
642 + class Stateful extends React.Component {
643 + componentDidUpdate() {
644 + ops.push('Update Stateful');
645 + }
646 +
647 + render() {
648 + return <div>Hello</div>;
649 + }
650 + }
651 +
652 + function Foo({condition}) {
653 + return condition
654 + ? <div>
655 + {<React.Fragment key="foo"><Stateful /></React.Fragment>}<span />
656 + </div>
657 + : <div>{[<Stateful />]}<span /></div>;
658 + }
659 +
660 + ReactNoop.render(<Foo condition={true} />);
661 + ReactNoop.flush();
662 +
663 + ReactNoop.render(<Foo condition={false} />);
664 + ReactNoop.flush();
665 +
666 + expect(ops).toEqual([]);
667 + expect(ReactNoop.getChildren()).toEqual([div(div(), span())]);
668 +
669 + ReactNoop.render(<Foo condition={true} />);
670 + ReactNoop.flush();
671 +
672 + expect(ops).toEqual([]);
673 + expect(ReactNoop.getChildren()).toEqual([div(div(), span())]);
674 + expectDev(console.error.calls.count()).toBe(1);
675 + expectDev(console.error.calls.argsFor(0)[0]).toContain(
676 + 'Each child in an array or iterator should have a unique "key" prop.',
677 + );
678 + });
679 +
680 + it('should preserve state when it does not change positions', function() {
681 + spyOn(console, 'error');
682 + var ops = [];
683 +
684 + class Stateful extends React.Component {
685 + componentDidUpdate() {
686 + ops.push('Update Stateful');
687 + }
688 +
689 + render() {
690 + return <div>Hello</div>;
691 + }
692 + }
693 +
694 + function Foo({condition}) {
695 + return condition
696 + ? [<span />, <React.Fragment><Stateful /></React.Fragment>]
697 + : [<span />, <React.Fragment><Stateful /></React.Fragment>];
698 + }
699 +
700 + ReactNoop.render(<Foo condition={true} />);
701 + ReactNoop.flush();
702 +
703 + ReactNoop.render(<Foo condition={false} />);
704 + ReactNoop.flush();
705 +
706 + expect(ops).toEqual(['Update Stateful']);
707 + expect(ReactNoop.getChildren()).toEqual([span(), div()]);
708 +
709 + ReactNoop.render(<Foo condition={true} />);
710 + ReactNoop.flush();
711 +
712 + expect(ops).toEqual(['Update Stateful', 'Update Stateful']);
713 + expect(ReactNoop.getChildren()).toEqual([span(), div()]);
714 + expectDev(console.error.calls.count()).toBe(3);
715 + for (let errorIndex = 0; errorIndex < 3; ++errorIndex) {
716 + expectDev(console.error.calls.argsFor(errorIndex)[0]).toContain(
717 + 'Each child in an array or iterator should have a unique "key" prop.',
718 + );
719 + }
720 + });
721 +});
packages/react/src/React.js
+7
@@ -25,6 +25,12 @@ if (__DEV__) {
25 cloneElement = ReactElementValidator.cloneElement;
26 }
27
28 +const REACT_FRAGMENT_TYPE =
29 + (typeof Symbol === 'function' &&
30 + Symbol.for &&
31 + Symbol.for('react.fragment')) ||
32 + 0xeacb;
33 +
34 var React = {
35 Children: {
36 map: ReactChildren.map,
@@ -37,6 +43,7 @@ var React = {
43 Component: ReactBaseClasses.Component,
44 PureComponent: ReactBaseClasses.PureComponent,
45 unstable_AsyncComponent: ReactBaseClasses.AsyncComponent,
46 + Fragment: REACT_FRAGMENT_TYPE,
47
48 createElement: createElement,
49 cloneElement: cloneElement,
packages/react/src/ReactElementValidator.js
+51 -2
@@ -35,6 +35,8 @@ if (__DEV__) {
35 return '#text';
36 } else if (typeof element.type === 'string') {
37 return element.type;
38 + } else if (element.type === REACT_FRAGMENT_TYPE) {
39 + return 'React.Fragment';
40 } else {
41 return element.type.displayName || element.type.name || 'Unknown';
42 }
@@ -54,6 +56,14 @@ if (__DEV__) {
56 stack += ReactDebugCurrentFrame.getStackAddendum() || '';
57 return stack;
58 };
59 +
60 + var REACT_FRAGMENT_TYPE =
61 + (typeof Symbol === 'function' &&
62 + Symbol.for &&
63 + Symbol.for('react.fragment')) ||
64 + 0xeacb;
65 +
66 + var VALID_FRAGMENT_PROPS = new Map([['children', true], ['key', true]]);
67 }
68
69 var ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator;
@@ -226,9 +236,44 @@ function validatePropTypes(element) {
236 }
237 }
238
239 +/**
240 + * Given a fragment, validate that it can only be provided with fragment props
241 + * @param {ReactElement} fragment
242 + */
243 +function validateFragmentProps(fragment) {
244 + currentlyValidatingElement = fragment;
245 +
246 + for (const key of Object.keys(fragment.props)) {
247 + if (!VALID_FRAGMENT_PROPS.has(key)) {
248 + warning(
249 + false,
250 + 'Invalid prop `%s` supplied to `React.Fragment`. ' +
251 + 'React.Fragment can only have `key` and `children` props.%s',
252 + key,
253 + getStackAddendum(),
254 + );
255 + break;
256 + }
257 + }
258 +
259 + if (fragment.ref !== null) {
260 + warning(
261 + false,
262 + 'Invalid attribute `ref` supplied to `React.Fragment`.%s',
263 + getStackAddendum(),
264 + );
265 + }
266 +
267 + currentlyValidatingElement = null;
268 +}
269 +
270 var ReactElementValidator = {
271 createElement: function(type, props, children) {
231 - var validType = typeof type === 'string' || typeof type === 'function';
272 + var validType =
273 + typeof type === 'string' ||
274 + typeof type === 'function' ||
275 + typeof type === 'symbol' ||
276 + typeof type === 'number';
277 // We warn in this case but don't throw. We expect the element creation to
278 // succeed and there will likely be errors in render.
279 if (!validType) {
@@ -282,7 +327,11 @@ var ReactElementValidator = {
327 }
328 }
329
285 - validatePropTypes(element);
330 + if (typeof type === 'symbol' && type === REACT_FRAGMENT_TYPE) {
331 + validateFragmentProps(element);
332 + } else {
333 + validatePropTypes(element);
334 + }
335
336 return element;
337 },
packages/react/src/__tests__/ReactElementValidator-test.js
+3 -9
@@ -267,10 +267,9 @@ describe('ReactElementValidator', () => {
267 React.createElement(undefined);
268 React.createElement(null);
269 React.createElement(true);
270 - React.createElement(123);
270 React.createElement({x: 17});
271 React.createElement({});
273 - expectDev(console.error.calls.count()).toBe(6);
272 + expectDev(console.error.calls.count()).toBe(5);
273 expectDev(console.error.calls.argsFor(0)[0]).toBe(
274 'Warning: React.createElement: type is invalid -- expected a string ' +
275 '(for built-in components) or a class/function (for composite ' +
@@ -288,23 +287,18 @@ describe('ReactElementValidator', () => {
287 'components) but got: boolean.',
288 );
289 expectDev(console.error.calls.argsFor(3)[0]).toBe(
291 - 'Warning: React.createElement: type is invalid -- expected a string ' +
292 - '(for built-in components) or a class/function (for composite ' +
293 - 'components) but got: number.',
294 - );
295 - expectDev(console.error.calls.argsFor(4)[0]).toBe(
290 'Warning: React.createElement: type is invalid -- expected a string ' +
291 '(for built-in components) or a class/function (for composite ' +
292 'components) but got: object.',
293 );
300 - expectDev(console.error.calls.argsFor(5)[0]).toBe(
294 + expectDev(console.error.calls.argsFor(4)[0]).toBe(
295 'Warning: React.createElement: type is invalid -- expected a string ' +
296 '(for built-in components) or a class/function (for composite ' +
297 'components) but got: object. You likely forgot to export your ' +
298 "component from the file it's defined in.",
299 );
300 React.createElement('div');
307 - expectDev(console.error.calls.count()).toBe(6);
301 + expectDev(console.error.calls.count()).toBe(5);
302 });
303
304 it('includes the owner name when passing null, undefined, boolean, or number', () => {
packages/react/src/__tests__/ReactJSXElementValidator-test.js
+87 -28
@@ -11,7 +11,6 @@
11
12 // TODO: All these warnings should become static errors using Flow instead
13 // of dynamic errors when using JSX with Flow.
14 -
14 var React;
15 var ReactDOM;
16 var ReactTestUtils;
@@ -51,7 +50,9 @@ describe('ReactJSXElementValidator', () => {
50 it('warns for keys for arrays of elements in children position', () => {
51 spyOn(console, 'error');
52
54 - void <Component>{[<Component />, <Component />]}</Component>;
53 + ReactTestUtils.renderIntoDocument(
54 + <Component>{[<Component />, <Component />]}</Component>,
55 + );
56
57 expectDev(console.error.calls.count()).toBe(1);
58 expectDev(console.error.calls.argsFor(0)[0]).toContain(
@@ -84,6 +85,49 @@ describe('ReactJSXElementValidator', () => {
85 );
86 });
87
88 + it('warns for fragments with illegal attributes', () => {
89 + spyOn(console, 'error');
90 +
91 + class Foo extends React.Component {
92 + render() {
93 + return <React.Fragment a={1} b={2}>hello</React.Fragment>;
94 + }
95 + }
96 +
97 + ReactTestUtils.renderIntoDocument(<Foo />);
98 +
99 + expectDev(console.error.calls.count()).toBe(1);
100 + expectDev(console.error.calls.argsFor(0)[0]).toContain('Invalid prop `');
101 + expectDev(console.error.calls.argsFor(0)[0]).toContain(
102 + '` supplied to `React.Fragment`. React.Fragment ' +
103 + 'can only have `key` and `children` props.',
104 + );
105 + });
106 +
107 + it('warns for fragments with refs', () => {
108 + spyOn(console, 'error');
109 +
110 + class Foo extends React.Component {
111 + render() {
112 + return (
113 + <React.Fragment
114 + ref={bar => {
115 + this.foo = bar;
116 + }}>
117 + hello
118 + </React.Fragment>
119 + );
120 + }
121 + }
122 +
123 + ReactTestUtils.renderIntoDocument(<Foo />);
124 +
125 + expectDev(console.error.calls.count()).toBe(1);
126 + expectDev(console.error.calls.argsFor(0)[0]).toContain(
127 + 'Invalid attribute `ref` supplied to `React.Fragment`.',
128 + );
129 + });
130 +
131 it('warns for keys for iterables of elements in rest args', () => {
132 spyOn(console, 'error');
133
@@ -99,7 +143,7 @@ describe('ReactJSXElementValidator', () => {
143 },
144 };
145
102 - void <Component>{iterable}</Component>;
146 + ReactTestUtils.renderIntoDocument(<Component>{iterable}</Component>);
147
148 expectDev(console.error.calls.count()).toBe(1);
149 expectDev(console.error.calls.argsFor(0)[0]).toContain(
@@ -107,17 +151,43 @@ describe('ReactJSXElementValidator', () => {
151 );
152 });
153
110 - it('does not warns for arrays of elements with keys', () => {
154 + it('does not warn for fragments of multiple elements without keys', () => {
155 + ReactTestUtils.renderIntoDocument(
156 + <React.Fragment>
157 + <span>1</span>
158 + <span>2</span>
159 + </React.Fragment>,
160 + );
161 + });
162 +
163 + it('warns for fragments of multiple elements with same key', () => {
164 spyOn(console, 'error');
165
113 - void (
114 - <Component>{[<Component key="#1" />, <Component key="#2" />]}</Component>
166 + ReactTestUtils.renderIntoDocument(
167 + <React.Fragment>
168 + <span key="a">1</span>
169 + <span key="a">2</span>
170 + <span key="b">3</span>
171 + </React.Fragment>,
172 + );
173 +
174 + expectDev(console.error.calls.count()).toBe(1);
175 + expectDev(console.error.calls.argsFor(0)[0]).toContain(
176 + 'Encountered two children with the same key, `a`.',
177 + );
178 + });
179 +
180 + it('does not warn for arrays of elements with keys', () => {
181 + spyOn(console, 'error');
182 +
183 + ReactTestUtils.renderIntoDocument(
184 + <Component>{[<Component key="#1" />, <Component key="#2" />]}</Component>,
185 );
186
187 expectDev(console.error.calls.count()).toBe(0);
188 });
189
120 - it('does not warns for iterable elements with keys', () => {
190 + it('does not warn for iterable elements with keys', () => {
191 spyOn(console, 'error');
192
193 var iterable = {
@@ -135,7 +205,7 @@ describe('ReactJSXElementValidator', () => {
205 },
206 };
207
138 - void <Component>{iterable}</Component>;
208 + ReactTestUtils.renderIntoDocument(<Component>{iterable}</Component>);
209
210 expectDev(console.error.calls.count()).toBe(0);
211 });
@@ -156,25 +226,22 @@ describe('ReactJSXElementValidator', () => {
226 };
227 iterable.entries = iterable['@@iterator'];
228
159 - void <Component>{iterable}</Component>;
229 + ReactTestUtils.renderIntoDocument(<Component>{iterable}</Component>);
230
231 expectDev(console.error.calls.count()).toBe(0);
232 });
233
234 it('does not warn when the element is directly as children', () => {
165 - spyOn(console, 'error');
166 -
167 - void <Component><Component /><Component /></Component>;
168 -
169 - expectDev(console.error.calls.count()).toBe(0);
235 + ReactTestUtils.renderIntoDocument(
236 + <Component>
237 + <Component />
238 + <Component />
239 + </Component>,
240 + );
241 });
242
243 it('does not warn when the child array contains non-elements', () => {
173 - spyOn(console, 'error');
174 -
244 void <Component>{[{}, {}]}</Component>;
176 -
177 - expectDev(console.error.calls.count()).toBe(0);
245 });
246
247 it('should give context for PropType errors in nested components.', () => {
@@ -246,14 +313,12 @@ describe('ReactJSXElementValidator', () => {
313 var Undefined = undefined;
314 var Null = null;
315 var True = true;
249 - var Num = 123;
316 var Div = 'div';
317 spyOn(console, 'error');
318 void <Undefined />;
319 void <Null />;
320 void <True />;
255 - void <Num />;
256 - expectDev(console.error.calls.count()).toBe(4);
321 + expectDev(console.error.calls.count()).toBe(3);
322 expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe(
323 'Warning: React.createElement: type is invalid -- expected a string ' +
324 '(for built-in components) or a class/function (for composite ' +
@@ -273,14 +338,8 @@ describe('ReactJSXElementValidator', () => {
338 'components) but got: boolean.' +
339 '\n\nCheck your code at **.',
340 );
276 - expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(3)[0])).toBe(
277 - 'Warning: React.createElement: type is invalid -- expected a string ' +
278 - '(for built-in components) or a class/function (for composite ' +
279 - 'components) but got: number.' +
280 - '\n\nCheck your code at **.',
281 - );
341 void <Div />;
283 - expectDev(console.error.calls.count()).toBe(4);
342 + expectDev(console.error.calls.count()).toBe(3);
343 });
344
345 it('should check default prop values', () => {