Skip to content

Commit e11dc40

Browse files
clemmyEthan-Arrowood
authored andcommitted
Add Fragment as a named export to React (facebook#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
1 parent 65adc20 commit e11dc40

File tree

10 files changed

+1120
-149
lines changed

10 files changed

+1120
-149
lines changed

packages/react-dom/src/__tests__/ReactDOMServerIntegration-test.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,55 @@ describe('ReactDOMServerIntegration', () => {
403403
expect(parent.childNodes[2].tagName).toBe('P');
404404
});
405405

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+
406455
itRenders('an iterable', async render => {
407456
const threeDivIterable = {
408457
'@@iterator': function() {
@@ -435,6 +484,7 @@ describe('ReactDOMServerIntegration', () => {
435484
// but server returns empty HTML. So we compare parent text.
436485
expect((await render(<div>{''}</div>)).textContent).toBe('');
437486

487+
expect(await render(<React.Fragment />)).toBe(null);
438488
expect(await render([])).toBe(null);
439489
expect(await render(false)).toBe(null);
440490
expect(await render(true)).toBe(null);

packages/react-dom/src/server/ReactPartialRenderer.js

Lines changed: 59 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ var escapeTextContentForBrowser = require('../shared/escapeTextContentForBrowser
3131
var isCustomComponent = require('../shared/isCustomComponent');
3232
var omittedCloseTags = require('../shared/omittedCloseTags');
3333

34+
var REACT_FRAGMENT_TYPE =
35+
(typeof Symbol === 'function' &&
36+
Symbol.for &&
37+
Symbol.for('react.fragment')) ||
38+
0xeacb;
39+
3440
// Based on reading the React.Children implementation. TODO: type this somewhere?
3541
type ReactNode = string | number | ReactElement;
3642
type FlatReactChildren = Array<null | ReactNode>;
@@ -206,6 +212,22 @@ function getNonChildrenInnerMarkup(props) {
206212
return null;
207213
}
208214

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+
209231
function flattenOptionChildren(children: mixed): string {
210232
var content = '';
211233
// Flatten children and warn if they aren't strings or numbers;
@@ -482,14 +504,8 @@ class ReactDOMServerRenderer {
482504
makeStaticMarkup: boolean;
483505

484506
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+
493509
var topFrame: Frame = {
494510
// Assume all trees start in the HTML namespace (not totally true, but
495511
// this is what we did historically)
@@ -569,26 +585,42 @@ class ReactDOMServerRenderer {
569585
({child: nextChild, context} = resolve(child, context));
570586
if (nextChild === null || nextChild === false) {
571587
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 = [];
591599
}
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);
592624
}
593625
}
594626
}

packages/react-noop-renderer/src/ReactNoop.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -512,7 +512,8 @@ var ReactNoop = {
512512
log(
513513
' '.repeat(depth) +
514514
'- ' +
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]'),
516517
'[' + fiber.expirationTime + (fiber.pendingProps ? '*' : '') + ']',
517518
);
518519
if (fiber.updateQueue) {

0 commit comments

Comments
 (0)