From 284efed004d3829e171a3310650ad4013c887a5c Mon Sep 17 00:00:00 2001 From: Clement Hoang Date: Fri, 22 Sep 2017 14:12:25 -0700 Subject: [PATCH 01/43] Add Fragment as a named export to React --- src/isomorphic/ReactEntry.js | 1 + .../modern/class/ReactBaseClasses.js | 5 + .../class/__tests__/ReactFragment-test.js | 113 ++++++++++++++++++ .../__snapshots__/ReactFragment-test.js.snap | 10 ++ .../ReactJSXElementValidator-test.js | 13 ++ 5 files changed, 142 insertions(+) create mode 100644 src/isomorphic/modern/class/__tests__/ReactFragment-test.js create mode 100644 src/isomorphic/modern/class/__tests__/__snapshots__/ReactFragment-test.js.snap diff --git a/src/isomorphic/ReactEntry.js b/src/isomorphic/ReactEntry.js index 52415de1da489..76cdc447af92f 100644 --- a/src/isomorphic/ReactEntry.js +++ b/src/isomorphic/ReactEntry.js @@ -39,6 +39,7 @@ var React = { Component: ReactBaseClasses.Component, PureComponent: ReactBaseClasses.PureComponent, unstable_AsyncComponent: ReactBaseClasses.AsyncComponent, + Fragment: ReactBaseClasses.Fragment, createElement: createElement, cloneElement: cloneElement, diff --git a/src/isomorphic/modern/class/ReactBaseClasses.js b/src/isomorphic/modern/class/ReactBaseClasses.js index 5c7c082125162..ad59165a19974 100644 --- a/src/isomorphic/modern/class/ReactBaseClasses.js +++ b/src/isomorphic/modern/class/ReactBaseClasses.js @@ -161,8 +161,13 @@ asyncComponentPrototype.render = function() { return this.props.children; }; +function ReactFragment(props) { + return props.children; +} + module.exports = { Component: ReactComponent, PureComponent: ReactPureComponent, AsyncComponent: ReactAsyncComponent, + Fragment: ReactFragment, }; diff --git a/src/isomorphic/modern/class/__tests__/ReactFragment-test.js b/src/isomorphic/modern/class/__tests__/ReactFragment-test.js new file mode 100644 index 0000000000000..9394b86ee7d50 --- /dev/null +++ b/src/isomorphic/modern/class/__tests__/ReactFragment-test.js @@ -0,0 +1,113 @@ +/** + * Copyright 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @emails react-core + */ +'use strict'; + +let React; +let ReactDOM; +let createRenderer; +let ReactTestRenderer; +let ReactNoop; +let ReactNative; +let UIManager; +let createReactNativeComponentClass; +let ReactDOMFeatureFlags = require('ReactDOMFeatureFlags'); +let element; + +describe('ReactFragment', () => { + beforeEach(() => { + jest.resetModules(); + + React = require('react'); + + element = ( + + hello world + + ); + }); + + it('should render via native renderer', () => { + ReactNative = require('react-native'); + UIManager = require('UIManager'); + createReactNativeComponentClass = require('createReactNativeComponentClass'); + + const View = createReactNativeComponentClass('View', () => ({ + validAttributes: {foo: true}, + uiViewClassName: 'View', + })); + const Text = createReactNativeComponentClass('Text', () => ({ + validAttributes: {foo: true}, + uiViewClassName: 'Text', + })); + + ReactNative.render( + + + 1 + 2 + + , + 11, + ); + + expect(UIManager.__dumpHierarchyForJestTestsOnly()).toMatchSnapshot(); + }); + + it('should render via shallow renderer', () => { + createRenderer = require('react-test-renderer/shallow').createRenderer; + + const shallowRenderer = createRenderer(); + shallowRenderer.render(element); + + expect(shallowRenderer.getRenderOutput()).toEqual([ + 'hello ', + world, + ]); + }); + + it('should render via test renderer', () => { + ReactTestRenderer = require('react-test-renderer'); + + const renderer = ReactTestRenderer.create(element); + + expect(renderer.toJSON()).toEqual([ + 'hello ', + { + type: 'span', + props: {}, + children: ['world'], + }, + ]); + }); + + it('should render via noop renderer', () => { + ReactNoop = require('react-noop-renderer'); + + ReactNoop.render(element); + ReactNoop.flush(); + + expect(ReactNoop.getChildren()).toEqual([ + {text: 'hello '}, + {type: 'span', children: [], prop: undefined}, + ]); + }); + + if (ReactDOMFeatureFlags.useFiber) { + it('should render via ReactDOM', () => { + ReactDOM = require('react-dom'); + + const container = document.createElement('div'); + ReactDOM.render(element, container); + + expect(container.innerHTML).toEqual('hello world'); + }); + } +}); diff --git a/src/isomorphic/modern/class/__tests__/__snapshots__/ReactFragment-test.js.snap b/src/isomorphic/modern/class/__tests__/__snapshots__/ReactFragment-test.js.snap new file mode 100644 index 0000000000000..e59441aa9714f --- /dev/null +++ b/src/isomorphic/modern/class/__tests__/__snapshots__/ReactFragment-test.js.snap @@ -0,0 +1,10 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ReactFragment should render via native renderer 1`] = ` +" {} + View null + Text {\\"foo\\":\\"a\\"} + RCTRawText {\\"text\\":\\"1\\"} + Text {\\"foo\\":\\"b\\"} + RCTRawText {\\"text\\":\\"2\\"}" +`; diff --git a/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js b/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js index 8cf5ee66dd7f0..9cc945dd2231b 100644 --- a/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js +++ b/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js @@ -107,6 +107,19 @@ describe('ReactJSXElementValidator', () => { ); }); + it('does not warns for fragments of multiple elements', () => { + spyOn(console, 'error'); + + void ( + + 1 + 2 + + ); + + expectDev(console.error.calls.count()).toBe(0); + }); + it('does not warns for arrays of elements with keys', () => { spyOn(console, 'error'); From c26914b3ff0ce0c289834a7e5581d4dc6fb23ae3 Mon Sep 17 00:00:00 2001 From: Clement Hoang Date: Fri, 22 Sep 2017 17:25:27 -0700 Subject: [PATCH 02/43] Remove extra tests for Fragment --- .../class/__tests__/ReactFragment-test.js | 97 ++----------------- .../__snapshots__/ReactFragment-test.js.snap | 10 -- .../ReactJSXElementValidator-test.js | 7 +- 3 files changed, 14 insertions(+), 100 deletions(-) delete mode 100644 src/isomorphic/modern/class/__tests__/__snapshots__/ReactFragment-test.js.snap diff --git a/src/isomorphic/modern/class/__tests__/ReactFragment-test.js b/src/isomorphic/modern/class/__tests__/ReactFragment-test.js index 9394b86ee7d50..34580abc42db9 100644 --- a/src/isomorphic/modern/class/__tests__/ReactFragment-test.js +++ b/src/isomorphic/modern/class/__tests__/ReactFragment-test.js @@ -10,87 +10,17 @@ */ 'use strict'; -let React; -let ReactDOM; -let createRenderer; -let ReactTestRenderer; -let ReactNoop; -let ReactNative; -let UIManager; -let createReactNativeComponentClass; -let ReactDOMFeatureFlags = require('ReactDOMFeatureFlags'); -let element; +const React = require('react'); +const ReactNoop = require('react-noop-renderer'); -describe('ReactFragment', () => { - beforeEach(() => { - jest.resetModules(); - - React = require('react'); - - element = ( - - hello world - - ); - }); - - it('should render via native renderer', () => { - ReactNative = require('react-native'); - UIManager = require('UIManager'); - createReactNativeComponentClass = require('createReactNativeComponentClass'); - - const View = createReactNativeComponentClass('View', () => ({ - validAttributes: {foo: true}, - uiViewClassName: 'View', - })); - const Text = createReactNativeComponentClass('Text', () => ({ - validAttributes: {foo: true}, - uiViewClassName: 'Text', - })); - - ReactNative.render( - - - 1 - 2 - - , - 11, - ); - - expect(UIManager.__dumpHierarchyForJestTestsOnly()).toMatchSnapshot(); - }); - - it('should render via shallow renderer', () => { - createRenderer = require('react-test-renderer/shallow').createRenderer; - - const shallowRenderer = createRenderer(); - shallowRenderer.render(element); - - expect(shallowRenderer.getRenderOutput()).toEqual([ - 'hello ', - world, - ]); - }); - - it('should render via test renderer', () => { - ReactTestRenderer = require('react-test-renderer'); - - const renderer = ReactTestRenderer.create(element); - - expect(renderer.toJSON()).toEqual([ - 'hello ', - { - type: 'span', - props: {}, - children: ['world'], - }, - ]); - }); +const element = ( + + hello world + +); +describe('ReactFragment', () => { it('should render via noop renderer', () => { - ReactNoop = require('react-noop-renderer'); - ReactNoop.render(element); ReactNoop.flush(); @@ -99,15 +29,4 @@ describe('ReactFragment', () => { {type: 'span', children: [], prop: undefined}, ]); }); - - if (ReactDOMFeatureFlags.useFiber) { - it('should render via ReactDOM', () => { - ReactDOM = require('react-dom'); - - const container = document.createElement('div'); - ReactDOM.render(element, container); - - expect(container.innerHTML).toEqual('hello world'); - }); - } }); diff --git a/src/isomorphic/modern/class/__tests__/__snapshots__/ReactFragment-test.js.snap b/src/isomorphic/modern/class/__tests__/__snapshots__/ReactFragment-test.js.snap deleted file mode 100644 index e59441aa9714f..0000000000000 --- a/src/isomorphic/modern/class/__tests__/__snapshots__/ReactFragment-test.js.snap +++ /dev/null @@ -1,10 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ReactFragment should render via native renderer 1`] = ` -" {} - View null - Text {\\"foo\\":\\"a\\"} - RCTRawText {\\"text\\":\\"1\\"} - Text {\\"foo\\":\\"b\\"} - RCTRawText {\\"text\\":\\"2\\"}" -`; diff --git a/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js b/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js index 9cc945dd2231b..89bedef354dd7 100644 --- a/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js +++ b/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js @@ -177,7 +177,12 @@ describe('ReactJSXElementValidator', () => { it('does not warn when the element is directly as children', () => { spyOn(console, 'error'); - void ; + void ( + + + + + ); expectDev(console.error.calls.count()).toBe(0); }); From adb7c61ebb6747fcca0b2ea390c333cd49d0e1cc Mon Sep 17 00:00:00 2001 From: Clement Hoang Date: Wed, 11 Oct 2017 15:29:44 -0700 Subject: [PATCH 03/43] Change React.Fragment export to be a string '#fragment' --- src/isomorphic/modern/class/ReactBaseClasses.js | 4 +--- src/renderers/shared/fiber/ReactFiber.js | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/isomorphic/modern/class/ReactBaseClasses.js b/src/isomorphic/modern/class/ReactBaseClasses.js index ad59165a19974..a5bf43497a9b9 100644 --- a/src/isomorphic/modern/class/ReactBaseClasses.js +++ b/src/isomorphic/modern/class/ReactBaseClasses.js @@ -161,9 +161,7 @@ asyncComponentPrototype.render = function() { return this.props.children; }; -function ReactFragment(props) { - return props.children; -} +const ReactFragment = '#fragment'; module.exports = { Component: ReactComponent, diff --git a/src/renderers/shared/fiber/ReactFiber.js b/src/renderers/shared/fiber/ReactFiber.js index 633911db34134..9330d682eb9a4 100644 --- a/src/renderers/shared/fiber/ReactFiber.js +++ b/src/renderers/shared/fiber/ReactFiber.js @@ -305,8 +305,7 @@ exports.createFiberFromElement = function( } const fiber = createFiberFromElementType( - element.type, - element.key, + element, internalContextTag, owner, ); @@ -346,18 +345,26 @@ exports.createFiberFromText = function( }; function createFiberFromElementType( - type: mixed, - key: null | string, + element: ReactElement, 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; } else if (typeof type === 'string') { + // special case for fragments, we create the fiber and return early + if (type === '#fragment') { + fiber = createFiber(Fragment, null, internalContextTag); + if (element.props.children && element.props.children.length) { + fiber.pendingProps = element.props.children; + } + return fiber; + } fiber = createFiber(HostComponent, key, internalContextTag); fiber.type = type; } else if ( @@ -398,6 +405,7 @@ function createFiberFromElementType( info, ); } + fiber.pendingProps = element.props; return fiber; } From 3804288c24e688fdf311f05a57b81eafcd4f018c Mon Sep 17 00:00:00 2001 From: Clement Hoang Date: Wed, 11 Oct 2017 16:06:50 -0700 Subject: [PATCH 04/43] Fix fragment special case to work with 1 child --- src/renderers/shared/fiber/ReactFiber.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/renderers/shared/fiber/ReactFiber.js b/src/renderers/shared/fiber/ReactFiber.js index 9330d682eb9a4..80433b7a013e1 100644 --- a/src/renderers/shared/fiber/ReactFiber.js +++ b/src/renderers/shared/fiber/ReactFiber.js @@ -360,9 +360,7 @@ function createFiberFromElementType( // special case for fragments, we create the fiber and return early if (type === '#fragment') { fiber = createFiber(Fragment, null, internalContextTag); - if (element.props.children && element.props.children.length) { - fiber.pendingProps = element.props.children; - } + fiber.pendingProps = element.props.children; return fiber; } fiber = createFiber(HostComponent, key, internalContextTag); From f201879c3fcd66605e678ca35c9a797e78a170fa Mon Sep 17 00:00:00 2001 From: Clement Hoang Date: Wed, 11 Oct 2017 16:18:53 -0700 Subject: [PATCH 05/43] Add single child test for fragment export --- .../class/__tests__/ReactFragment-test.js | 29 ++++++++++++++----- .../ReactJSXElementValidator-test.js | 2 +- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/isomorphic/modern/class/__tests__/ReactFragment-test.js b/src/isomorphic/modern/class/__tests__/ReactFragment-test.js index 34580abc42db9..29094dbb2d608 100644 --- a/src/isomorphic/modern/class/__tests__/ReactFragment-test.js +++ b/src/isomorphic/modern/class/__tests__/ReactFragment-test.js @@ -13,14 +13,29 @@ const React = require('react'); const ReactNoop = require('react-noop-renderer'); -const element = ( - - hello world - -); - describe('ReactFragment', () => { - it('should render via noop renderer', () => { + it('should render a single child via noop renderer', () => { + const element = ( + + foo + + ); + + ReactNoop.render(element); + ReactNoop.flush(); + + expect(ReactNoop.getChildren()).toEqual([ + {type: 'span', children: [], prop: undefined}, + ]); + }); + + it('should render multiple children via noop renderer', () => { + const element = ( + + hello world + + ); + ReactNoop.render(element); ReactNoop.flush(); diff --git a/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js b/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js index 89bedef354dd7..b8b52b64a09fc 100644 --- a/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js +++ b/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js @@ -107,7 +107,7 @@ describe('ReactJSXElementValidator', () => { ); }); - it('does not warns for fragments of multiple elements', () => { + it('does not warns for fragments of multiple elements without keys', () => { spyOn(console, 'error'); void ( From 3a21716870e3b8008de465bd56a6203c0151618b Mon Sep 17 00:00:00 2001 From: Clement Hoang Date: Thu, 12 Oct 2017 12:47:10 -0700 Subject: [PATCH 06/43] Move fragment definition to ReactEntry.js and render components for key warning tests --- src/isomorphic/ReactEntry.js | 2 +- .../modern/class/ReactBaseClasses.js | 3 -- .../ReactJSXElementValidator-test.js | 30 +++++++++++-------- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/isomorphic/ReactEntry.js b/src/isomorphic/ReactEntry.js index 76cdc447af92f..ecdc1c21834e1 100644 --- a/src/isomorphic/ReactEntry.js +++ b/src/isomorphic/ReactEntry.js @@ -39,7 +39,7 @@ var React = { Component: ReactBaseClasses.Component, PureComponent: ReactBaseClasses.PureComponent, unstable_AsyncComponent: ReactBaseClasses.AsyncComponent, - Fragment: ReactBaseClasses.Fragment, + Fragment: '#fragment', createElement: createElement, cloneElement: cloneElement, diff --git a/src/isomorphic/modern/class/ReactBaseClasses.js b/src/isomorphic/modern/class/ReactBaseClasses.js index a5bf43497a9b9..5c7c082125162 100644 --- a/src/isomorphic/modern/class/ReactBaseClasses.js +++ b/src/isomorphic/modern/class/ReactBaseClasses.js @@ -161,11 +161,8 @@ asyncComponentPrototype.render = function() { return this.props.children; }; -const ReactFragment = '#fragment'; - module.exports = { Component: ReactComponent, PureComponent: ReactPureComponent, AsyncComponent: ReactAsyncComponent, - Fragment: ReactFragment, }; diff --git a/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js b/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js index b8b52b64a09fc..8ac0fafdae176 100644 --- a/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js +++ b/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js @@ -11,7 +11,7 @@ // TODO: All these warnings should become static errors using Flow instead // of dynamic errors when using JSX with Flow. - +var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags'); var React; var ReactDOM; var ReactTestUtils; @@ -51,7 +51,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( @@ -99,7 +101,7 @@ describe('ReactJSXElementValidator', () => { }, }; - void {iterable}; + ReactTestUtils.renderIntoDocument({iterable}); expectDev(console.error.calls.count()).toBe(1); expectDev(console.error.calls.argsFor(0)[0]).toContain( @@ -110,12 +112,14 @@ describe('ReactJSXElementValidator', () => { it('does not warns for fragments of multiple elements without keys', () => { spyOn(console, 'error'); - void ( - - 1 - 2 - - ); + if (ReactDOMFeatureFlags.useFiber) { + ReactTestUtils.renderIntoDocument( + + 1 + 2 + , + ); + } expectDev(console.error.calls.count()).toBe(0); }); @@ -123,8 +127,8 @@ describe('ReactJSXElementValidator', () => { it('does not warns for arrays of elements with keys', () => { spyOn(console, 'error'); - void ( - {[, ]} + ReactTestUtils.renderIntoDocument( + {[, ]}, ); expectDev(console.error.calls.count()).toBe(0); @@ -148,7 +152,7 @@ describe('ReactJSXElementValidator', () => { }, }; - void {iterable}; + ReactTestUtils.renderIntoDocument({iterable}); expectDev(console.error.calls.count()).toBe(0); }); @@ -169,7 +173,7 @@ describe('ReactJSXElementValidator', () => { }; iterable.entries = iterable['@@iterator']; - void {iterable}; + ReactTestUtils.renderIntoDocument({iterable}); expectDev(console.error.calls.count()).toBe(0); }); From 480e8d176e694bbe0860a895a124ce29efa6a274 Mon Sep 17 00:00:00 2001 From: Clement Hoang Date: Thu, 12 Oct 2017 13:03:54 -0700 Subject: [PATCH 07/43] Inline createFiberFromElementType into createFiberFromElement --- src/renderers/shared/fiber/ReactFiber.js | 51 +++++++++++++++++++----- 1 file changed, 41 insertions(+), 10 deletions(-) diff --git a/src/renderers/shared/fiber/ReactFiber.js b/src/renderers/shared/fiber/ReactFiber.js index 80433b7a013e1..75d3769123cbd 100644 --- a/src/renderers/shared/fiber/ReactFiber.js +++ b/src/renderers/shared/fiber/ReactFiber.js @@ -356,15 +356,15 @@ function createFiberFromElementType( ? createFiber(ClassComponent, key, internalContextTag) : createFiber(IndeterminateComponent, key, internalContextTag); fiber.type = type; - } else if (typeof type === 'string') { + fiber.pendingProps = element.props; + } else if (typeof type === 'string' && type === '#fragment') { // special case for fragments, we create the fiber and return early - if (type === '#fragment') { - fiber = createFiber(Fragment, null, internalContextTag); - fiber.pendingProps = element.props.children; - return fiber; - } + fiber = createFiber(Fragment, null, internalContextTag); + fiber.pendingProps = element.props.children; + } else if (typeof type === 'string') { fiber = createFiber(HostComponent, key, internalContextTag); fiber.type = type; + fiber.pendingProps = element.props; } else if ( typeof type === 'object' && type !== null && @@ -377,6 +377,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__) { @@ -390,7 +391,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 + '`.'; } @@ -402,12 +403,42 @@ function createFiberFromElementType( type == null ? type : typeof type, info, ); + fiber.pendingProps = element.props; } - fiber.pendingProps = element.props; + + fiber.pendingWorkPriority = priorityLevel; + + if (__DEV__) { + fiber._debugSource = element._source; + fiber._debugOwner = element._owner; + } + return fiber; -} +}; + +exports.createFiberFromFragment = function( + elements: ReactFragment, + internalContextTag: TypeOfInternalContext, + priorityLevel: PriorityLevel, +): 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.pendingWorkPriority = priorityLevel; + return fiber; +}; -exports.createFiberFromElementType = createFiberFromElementType; +exports.createFiberFromText = function( + content: string, + internalContextTag: TypeOfInternalContext, + priorityLevel: PriorityLevel, +): Fiber { + const fiber = createFiber(HostText, null, internalContextTag); + fiber.pendingProps = content; + fiber.pendingWorkPriority = priorityLevel; + return fiber; +}; exports.createFiberFromHostInstanceForDeletion = function(): Fiber { const fiber = createFiber(HostComponent, null, NoContext); From 29e64729cdfe9820229f00d7e5ecb123491a44d9 Mon Sep 17 00:00:00 2001 From: Clement Hoang Date: Mon, 16 Oct 2017 15:04:57 -0700 Subject: [PATCH 08/43] Update reconciliation to special case fragments --- .../class/__tests__/ReactFragment-test.js | 56 ++++++++++++++++++- src/renderers/shared/fiber/ReactChildFiber.js | 37 +++++++++++- src/renderers/shared/fiber/ReactFiber.js | 2 +- 3 files changed, 89 insertions(+), 6 deletions(-) diff --git a/src/isomorphic/modern/class/__tests__/ReactFragment-test.js b/src/isomorphic/modern/class/__tests__/ReactFragment-test.js index 29094dbb2d608..9702ede7c760c 100644 --- a/src/isomorphic/modern/class/__tests__/ReactFragment-test.js +++ b/src/isomorphic/modern/class/__tests__/ReactFragment-test.js @@ -10,10 +10,16 @@ */ 'use strict'; -const React = require('react'); -const ReactNoop = require('react-noop-renderer'); +let React; +let ReactNoop; describe('ReactFragment', () => { + beforeEach(function() { + jest.resetModules(); + React = require('react'); + ReactNoop = require('react-noop-renderer'); + }); + it('should render a single child via noop renderer', () => { const element = ( @@ -44,4 +50,50 @@ describe('ReactFragment', () => { {type: 'span', children: [], prop: undefined}, ]); }); + + it('should preserve state of children', function() { + var instance = null; + + class Stateful extends React.Component { + render() { + instance = this; + return
Hello
; + } + } + + function Fragment({condition}) { + return condition ? ( + + + + ) : ( + + +
World
+
+ ); + } + + ReactNoop.render( +
+ +
+
, + ); + ReactNoop.flush(); + + var instanceA = instance; + + ReactNoop.render( +
+ +
+
, + ); + ReactNoop.flush(); + + var instanceB = instance; + + expect(instanceB).toBe(instanceA); + }); }); diff --git a/src/renderers/shared/fiber/ReactChildFiber.js b/src/renderers/shared/fiber/ReactChildFiber.js index 935b897b3ca1b..0a1f73c62ed2f 100644 --- a/src/renderers/shared/fiber/ReactChildFiber.js +++ b/src/renderers/shared/fiber/ReactChildFiber.js @@ -375,7 +375,22 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { element: ReactElement, expirationTime: ExpirationTime, ): Fiber { - if (current === null || current.type !== element.type) { + if ( + current !== null && + current.tag === Fragment && + element.type === '#fragment' + ) { + // Move based on index + const existing = useFiber(current, priority); + existing.ref = coerceRef(current, element); + existing.pendingProps = element.props.children; + existing.return = returnFiber; + if (__DEV__) { + existing._debugSource = element._source; + existing._debugOwner = element._owner; + } + return existing; + } else if (current === null || current.type !== element.type) { // Insert const created = createFiberFromElement( element, @@ -1047,7 +1062,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { for ( ; oldFiber !== null && !step.done; - newIdx++, (step = newChildren.next()) + newIdx++, step = newChildren.next() ) { if (oldFiber.index > newIdx) { nextOldFiber = oldFiber; @@ -1102,8 +1117,13 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { if (oldFiber === null) { // If we don't have any more existing children we can choose a fast path // since the rest will all be insertions. +<<<<<<< HEAD for (; !step.done; newIdx++, (step = newChildren.next())) { const newFiber = createChild(returnFiber, step.value, expirationTime); +======= + for (; !step.done; newIdx++, step = newChildren.next()) { + const newFiber = createChild(returnFiber, step.value, priority); +>>>>>>> Update reconciliation to special case fragments if (newFiber === null) { continue; } @@ -1123,7 +1143,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const existingChildren = mapRemainingChildren(returnFiber, oldFiber); // Keep scanning and use the map to restore deleted items as moves. - for (; !step.done; newIdx++, (step = newChildren.next())) { + for (; !step.done; newIdx++, step = newChildren.next()) { const newFiber = updateFromMap( existingChildren, returnFiber, @@ -1214,6 +1234,17 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { existing._debugOwner = element._owner; } return existing; + } else if (child.tag === Fragment && element.type === '#fragment') { + deleteRemainingChildren(returnFiber, child.sibling); + const existing = useFiber(child, priority); + existing.ref = coerceRef(child, element); + existing.pendingProps = element.props.children; + existing.return = returnFiber; + if (__DEV__) { + existing._debugSource = element._source; + existing._debugOwner = element._owner; + } + return existing; } else { deleteRemainingChildren(returnFiber, child); break; diff --git a/src/renderers/shared/fiber/ReactFiber.js b/src/renderers/shared/fiber/ReactFiber.js index 75d3769123cbd..6e50f0522c30d 100644 --- a/src/renderers/shared/fiber/ReactFiber.js +++ b/src/renderers/shared/fiber/ReactFiber.js @@ -359,7 +359,7 @@ function createFiberFromElementType( fiber.pendingProps = element.props; } else if (typeof type === 'string' && type === '#fragment') { // special case for fragments, we create the fiber and return early - fiber = createFiber(Fragment, null, internalContextTag); + fiber = createFiber(Fragment, key, internalContextTag); fiber.pendingProps = element.props.children; } else if (typeof type === 'string') { fiber = createFiber(HostComponent, key, internalContextTag); From e62c4b483ff95d9b27620b8b003412b3f68e9bd3 Mon Sep 17 00:00:00 2001 From: Clement Hoang Date: Mon, 16 Oct 2017 17:56:40 -0700 Subject: [PATCH 09/43] Use same semantics as implicit childsets for ReactFragment --- src/renderers/shared/fiber/ReactChildFiber.js | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/renderers/shared/fiber/ReactChildFiber.js b/src/renderers/shared/fiber/ReactChildFiber.js index 0a1f73c62ed2f..d813929bd1363 100644 --- a/src/renderers/shared/fiber/ReactChildFiber.js +++ b/src/renderers/shared/fiber/ReactChildFiber.js @@ -86,6 +86,7 @@ const isArray = Array.isArray; const { FunctionalComponent, ClassComponent, + HostRoot, HostText, HostPortal, CoroutineComponent, @@ -1392,6 +1393,32 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { if (isObject) { switch (newChild.$$typeof) { case REACT_ELEMENT_TYPE: + // If newChild is a top-level unkeyed fragment, don't create a fiber + if ( + newChild.type === '#fragment' && + newChild.key === null && + ( + returnFiber.tag === HostRoot || + returnFiber.tag === ClassComponent || + returnFiber.tag === FunctionalComponent + ) + ) { + if (Array.isArray(newChild.props.children)) { + return reconcileChildrenArray( + returnFiber, + currentFirstChild, + newChild.props.children, + priority + ); + } else { + return reconcileChildrenArray( + returnFiber, + currentFirstChild, + [newChild.props.children], + priority + ); + } + } return placeSingleChild( reconcileSingleElement( returnFiber, From e0c0a1ca2d12ee521f0a2f9896b14666cbf5ddae Mon Sep 17 00:00:00 2001 From: Clement Hoang Date: Mon, 16 Oct 2017 18:06:11 -0700 Subject: [PATCH 10/43] Add more fragment state preservation tests --- .../class/__tests__/ReactFragment-test.js | 81 +++++++++++++++++++ src/renderers/shared/fiber/ReactChildFiber.js | 10 +-- 2 files changed, 85 insertions(+), 6 deletions(-) diff --git a/src/isomorphic/modern/class/__tests__/ReactFragment-test.js b/src/isomorphic/modern/class/__tests__/ReactFragment-test.js index 9702ede7c760c..3c1c7937c708f 100644 --- a/src/isomorphic/modern/class/__tests__/ReactFragment-test.js +++ b/src/isomorphic/modern/class/__tests__/ReactFragment-test.js @@ -96,4 +96,85 @@ describe('ReactFragment', () => { expect(instanceB).toBe(instanceA); }); + + it('should not preserve state when switching to a nested fragment', function() { + var instance = null; + + class Stateful extends React.Component { + render() { + instance = this; + return
Hello
; + } + } + + function Fragment({condition}) { + return condition ? ( + + ) : ( + + + +
World
+
+ + + ); + } + ReactNoop.render(); + ReactNoop.flush(); + + var instanceA = instance; + + expect(instanceA).not.toBe(null); + + ReactNoop.render(); + ReactNoop.flush(); + + var instanceB = instance; + + expect(instanceB).not.toBe(instanceA); + }); + + it('should preserve state in a reorder', function() { + var instance = null; + + class Stateful extends React.Component { + render() { + instance = this; + return
Hello
; + } + } + + function Fragment({condition}) { + return condition ? ( + + +
World
+ +
+
+ ) : ( + + + +
World
+
+
+ + ); + } + ReactNoop.render(); + ReactNoop.flush(); + + var instanceA = instance; + + expect(instanceA).not.toBe(null); + + ReactNoop.render(); + ReactNoop.flush(); + + var instanceB = instance; + + expect(instanceB).toBe(instanceA); + }); }); diff --git a/src/renderers/shared/fiber/ReactChildFiber.js b/src/renderers/shared/fiber/ReactChildFiber.js index d813929bd1363..d7d4dbd9c3145 100644 --- a/src/renderers/shared/fiber/ReactChildFiber.js +++ b/src/renderers/shared/fiber/ReactChildFiber.js @@ -1397,25 +1397,23 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { if ( newChild.type === '#fragment' && newChild.key === null && - ( - returnFiber.tag === HostRoot || + (returnFiber.tag === HostRoot || returnFiber.tag === ClassComponent || - returnFiber.tag === FunctionalComponent - ) + returnFiber.tag === FunctionalComponent) ) { if (Array.isArray(newChild.props.children)) { return reconcileChildrenArray( returnFiber, currentFirstChild, newChild.props.children, - priority + priority, ); } else { return reconcileChildrenArray( returnFiber, currentFirstChild, [newChild.props.children], - priority + priority, ); } } From c74af40ffb54eb8d90d9f4e36d7924bff8eea655 Mon Sep 17 00:00:00 2001 From: Clement Hoang Date: Tue, 17 Oct 2017 11:27:31 -0700 Subject: [PATCH 11/43] Export symbol instead of string for fragments --- src/isomorphic/ReactEntry.js | 8 ++++++- .../classic/element/ReactElementValidator.js | 22 ++++++++++++++----- src/renderers/shared/fiber/ReactChildFiber.js | 14 +++++++++--- src/renderers/shared/fiber/ReactFiber.js | 9 ++++++-- 4 files changed, 42 insertions(+), 11 deletions(-) diff --git a/src/isomorphic/ReactEntry.js b/src/isomorphic/ReactEntry.js index ecdc1c21834e1..58881c256a9de 100644 --- a/src/isomorphic/ReactEntry.js +++ b/src/isomorphic/ReactEntry.js @@ -27,6 +27,12 @@ if (__DEV__) { cloneElement = ReactElementValidator.cloneElement; } +const REACT_FRAGMENT_TYPE = + (typeof Symbol === 'function' && + Symbol.for && + Symbol.for('react.fragment')) || + 0xad9c; + var React = { Children: { map: ReactChildren.map, @@ -39,7 +45,7 @@ var React = { Component: ReactBaseClasses.Component, PureComponent: ReactBaseClasses.PureComponent, unstable_AsyncComponent: ReactBaseClasses.AsyncComponent, - Fragment: '#fragment', + Fragment: REACT_FRAGMENT_TYPE, createElement: createElement, cloneElement: cloneElement, diff --git a/src/isomorphic/classic/element/ReactElementValidator.js b/src/isomorphic/classic/element/ReactElementValidator.js index 2c0a2271a10bf..9f932831ef10f 100644 --- a/src/isomorphic/classic/element/ReactElementValidator.js +++ b/src/isomorphic/classic/element/ReactElementValidator.js @@ -19,6 +19,12 @@ var ReactCurrentOwner = require('ReactCurrentOwner'); var ReactElement = require('ReactElement'); +const REACT_FRAGMENT_TYPE = + (typeof Symbol === 'function' && + Symbol.for && + Symbol.for('react.fragment')) || + 0xad9c; + if (__DEV__) { var checkPropTypes = require('prop-types/checkPropTypes'); var lowPriorityWarning = require('lowPriorityWarning'); @@ -95,9 +101,10 @@ function getCurrentComponentErrorInfo(parentType) { var info = getDeclarationErrorAddendum(); if (!info) { - var parentName = typeof parentType === 'string' - ? parentType - : parentType.displayName || parentType.name; + var parentName = + typeof parentType === 'string' + ? parentType + : parentType.displayName || parentType.name; if (parentName) { info = `\n\nCheck the top-level render call using <${parentName}>.`; } @@ -138,7 +145,9 @@ function validateExplicitKey(element, parentType) { element._owner !== ReactCurrentOwner.current ) { // Give the component that originally created this child. - childOwner = ` It was passed a child from ${getComponentName(element._owner)}.`; + childOwner = ` It was passed a child from ${getComponentName( + element._owner, + )}.`; } currentlyValidatingElement = element; @@ -229,7 +238,10 @@ function validatePropTypes(element) { var ReactElementValidator = { createElement: function(type, props, children) { - var validType = typeof type === 'string' || typeof type === 'function'; + var validType = + typeof type === 'string' || + typeof type === 'function' || + type === REACT_FRAGMENT_TYPE; // 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) { diff --git a/src/renderers/shared/fiber/ReactChildFiber.js b/src/renderers/shared/fiber/ReactChildFiber.js index d7d4dbd9c3145..ed67b5e8a2345 100644 --- a/src/renderers/shared/fiber/ReactChildFiber.js +++ b/src/renderers/shared/fiber/ReactChildFiber.js @@ -103,6 +103,11 @@ const FAUX_ITERATOR_SYMBOL = '@@iterator'; // Before Symbol spec. const REACT_ELEMENT_TYPE = (typeof Symbol === 'function' && Symbol.for && Symbol.for('react.element')) || 0xeac7; +const REACT_FRAGMENT_TYPE = + (typeof Symbol === 'function' && + Symbol.for && + Symbol.for('react.fragment')) || + 0xad9c; function getIteratorFn(maybeIterable: ?any): ?() => ?Iterator<*> { if (maybeIterable === null || typeof maybeIterable === 'undefined') { @@ -379,7 +384,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { if ( current !== null && current.tag === Fragment && - element.type === '#fragment' + element.type === REACT_FRAGMENT_TYPE ) { // Move based on index const existing = useFiber(current, priority); @@ -1235,7 +1240,10 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { existing._debugOwner = element._owner; } return existing; - } else if (child.tag === Fragment && element.type === '#fragment') { + } else if ( + child.tag === Fragment && + element.type === REACT_FRAGMENT_TYPE + ) { deleteRemainingChildren(returnFiber, child.sibling); const existing = useFiber(child, priority); existing.ref = coerceRef(child, element); @@ -1395,7 +1403,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { case REACT_ELEMENT_TYPE: // If newChild is a top-level unkeyed fragment, don't create a fiber if ( - newChild.type === '#fragment' && + newChild.type === REACT_FRAGMENT_TYPE && newChild.key === null && (returnFiber.tag === HostRoot || returnFiber.tag === ClassComponent || diff --git a/src/renderers/shared/fiber/ReactFiber.js b/src/renderers/shared/fiber/ReactFiber.js index 6e50f0522c30d..aa1ccf29e540e 100644 --- a/src/renderers/shared/fiber/ReactFiber.js +++ b/src/renderers/shared/fiber/ReactFiber.js @@ -43,6 +43,12 @@ var {NoEffect} = require('ReactTypeOfSideEffect'); var invariant = require('fbjs/lib/invariant'); +const REACT_FRAGMENT_TYPE = + (typeof Symbol === 'function' && + Symbol.for && + Symbol.for('react.fragment')) || + 0xad9c; + if (__DEV__) { var getComponentName = require('getComponentName'); var hasBadMapPolyfill = false; @@ -357,7 +363,7 @@ function createFiberFromElementType( : createFiber(IndeterminateComponent, key, internalContextTag); fiber.type = type; fiber.pendingProps = element.props; - } else if (typeof type === 'string' && type === '#fragment') { + } else if (type === REACT_FRAGMENT_TYPE) { // special case for fragments, we create the fiber and return early fiber = createFiber(Fragment, key, internalContextTag); fiber.pendingProps = element.props.children; @@ -403,7 +409,6 @@ function createFiberFromElementType( type == null ? type : typeof type, info, ); - fiber.pendingProps = element.props; } fiber.pendingWorkPriority = priorityLevel; From 1f9ef583035e7ec9f0a4c0dc87fd452b7b461521 Mon Sep 17 00:00:00 2001 From: Clement Hoang Date: Tue, 17 Oct 2017 13:51:57 -0700 Subject: [PATCH 12/43] Fix rebase breakages --- src/renderers/shared/fiber/ReactChildFiber.js | 35 +++--------- src/renderers/shared/fiber/ReactFiber.js | 57 ++----------------- 2 files changed, 15 insertions(+), 77 deletions(-) diff --git a/src/renderers/shared/fiber/ReactChildFiber.js b/src/renderers/shared/fiber/ReactChildFiber.js index ed67b5e8a2345..70a9cd34679f8 100644 --- a/src/renderers/shared/fiber/ReactChildFiber.js +++ b/src/renderers/shared/fiber/ReactChildFiber.js @@ -387,7 +387,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { element.type === REACT_FRAGMENT_TYPE ) { // Move based on index - const existing = useFiber(current, priority); + const existing = useFiber(current, expirationTime); existing.ref = coerceRef(current, element); existing.pendingProps = element.props.children; existing.return = returnFiber; @@ -1123,13 +1123,8 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { if (oldFiber === null) { // If we don't have any more existing children we can choose a fast path // since the rest will all be insertions. -<<<<<<< HEAD for (; !step.done; newIdx++, (step = newChildren.next())) { const newFiber = createChild(returnFiber, step.value, expirationTime); -======= - for (; !step.done; newIdx++, step = newChildren.next()) { - const newFiber = createChild(returnFiber, step.value, priority); ->>>>>>> Update reconciliation to special case fragments if (newFiber === null) { continue; } @@ -1240,12 +1235,9 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { existing._debugOwner = element._owner; } return existing; - } else if ( - child.tag === Fragment && - element.type === REACT_FRAGMENT_TYPE - ) { + } else if (child.tag === Fragment && element.type === REACT_FRAGMENT_TYPE) { deleteRemainingChildren(returnFiber, child.sibling); - const existing = useFiber(child, priority); + const existing = useFiber(child, expirationTime); existing.ref = coerceRef(child, element); existing.pendingProps = element.props.children; existing.return = returnFiber; @@ -1409,21 +1401,12 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber.tag === ClassComponent || returnFiber.tag === FunctionalComponent) ) { - if (Array.isArray(newChild.props.children)) { - return reconcileChildrenArray( - returnFiber, - currentFirstChild, - newChild.props.children, - priority, - ); - } else { - return reconcileChildrenArray( - returnFiber, - currentFirstChild, - [newChild.props.children], - priority, - ); - } + return reconcileChildrenArray( + returnFiber, + currentFirstChild, + Array.isArray(newChild.props.children) ? newChild.props.children : [newChild.props.children], + expirationTime, + ); } return placeSingleChild( reconcileSingleElement( diff --git a/src/renderers/shared/fiber/ReactFiber.js b/src/renderers/shared/fiber/ReactFiber.js index aa1ccf29e540e..df3c38a5a34d0 100644 --- a/src/renderers/shared/fiber/ReactFiber.js +++ b/src/renderers/shared/fiber/ReactFiber.js @@ -310,51 +310,6 @@ exports.createFiberFromElement = function( owner = element._owner; } - const fiber = createFiberFromElementType( - element, - 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( - element: ReactElement, - internalContextTag: TypeOfInternalContext, - debugOwner: null | Fiber, -): Fiber { let fiber; const {type, key} = element; if (typeof type === 'function') { @@ -411,37 +366,37 @@ function createFiberFromElementType( ); } - fiber.pendingWorkPriority = priorityLevel; - if (__DEV__) { fiber._debugSource = element._source; fiber._debugOwner = element._owner; } + fiber.expirationTime = expirationTime; + return fiber; }; exports.createFiberFromFragment = function( elements: ReactFragment, internalContextTag: TypeOfInternalContext, - priorityLevel: PriorityLevel, + 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.pendingWorkPriority = priorityLevel; + fiber.expirationTime = expirationTime; return fiber; }; exports.createFiberFromText = function( content: string, internalContextTag: TypeOfInternalContext, - priorityLevel: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { const fiber = createFiber(HostText, null, internalContextTag); fiber.pendingProps = content; - fiber.pendingWorkPriority = priorityLevel; + fiber.expirationTime = expirationTime; return fiber; }; From 14d3a1b47ea21811cf40ccb92c9f799259189629 Mon Sep 17 00:00:00 2001 From: Clement Hoang Date: Tue, 17 Oct 2017 13:53:27 -0700 Subject: [PATCH 13/43] Re-apply prettier at 1.2.2 --- .../classic/element/ReactElementValidator.js | 11 ++-- .../class/__tests__/ReactFragment-test.js | 66 +++++++++---------- src/renderers/shared/fiber/ReactChildFiber.js | 13 ++-- 3 files changed, 43 insertions(+), 47 deletions(-) diff --git a/src/isomorphic/classic/element/ReactElementValidator.js b/src/isomorphic/classic/element/ReactElementValidator.js index 9f932831ef10f..ea0b00e807c23 100644 --- a/src/isomorphic/classic/element/ReactElementValidator.js +++ b/src/isomorphic/classic/element/ReactElementValidator.js @@ -101,10 +101,9 @@ function getCurrentComponentErrorInfo(parentType) { var info = getDeclarationErrorAddendum(); if (!info) { - var parentName = - typeof parentType === 'string' - ? parentType - : parentType.displayName || parentType.name; + var parentName = typeof parentType === 'string' + ? parentType + : parentType.displayName || parentType.name; if (parentName) { info = `\n\nCheck the top-level render call using <${parentName}>.`; } @@ -145,9 +144,7 @@ function validateExplicitKey(element, parentType) { element._owner !== ReactCurrentOwner.current ) { // Give the component that originally created this child. - childOwner = ` It was passed a child from ${getComponentName( - element._owner, - )}.`; + childOwner = ` It was passed a child from ${getComponentName(element._owner)}.`; } currentlyValidatingElement = element; diff --git a/src/isomorphic/modern/class/__tests__/ReactFragment-test.js b/src/isomorphic/modern/class/__tests__/ReactFragment-test.js index 3c1c7937c708f..56d8154904f44 100644 --- a/src/isomorphic/modern/class/__tests__/ReactFragment-test.js +++ b/src/isomorphic/modern/class/__tests__/ReactFragment-test.js @@ -62,16 +62,14 @@ describe('ReactFragment', () => { } function Fragment({condition}) { - return condition ? ( - - - - ) : ( - - -
World
-
- ); + return condition + ? + + + : + +
World
+
; } ReactNoop.render( @@ -108,17 +106,15 @@ describe('ReactFragment', () => { } function Fragment({condition}) { - return condition ? ( - - ) : ( - - - -
World
-
- - - ); + return condition + ? + : + + +
World
+
+ + ; } ReactNoop.render(); ReactNoop.flush(); @@ -146,22 +142,20 @@ describe('ReactFragment', () => { } function Fragment({condition}) { - return condition ? ( - - -
World
- + return condition + ? + +
World
+ +
-
- ) : ( - - - -
World
-
-
- - ); + : + + +
World
+
+
+ ; } ReactNoop.render(); ReactNoop.flush(); diff --git a/src/renderers/shared/fiber/ReactChildFiber.js b/src/renderers/shared/fiber/ReactChildFiber.js index 70a9cd34679f8..0e5963130fdc7 100644 --- a/src/renderers/shared/fiber/ReactChildFiber.js +++ b/src/renderers/shared/fiber/ReactChildFiber.js @@ -1068,7 +1068,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { for ( ; oldFiber !== null && !step.done; - newIdx++, step = newChildren.next() + newIdx++, (step = newChildren.next()) ) { if (oldFiber.index > newIdx) { nextOldFiber = oldFiber; @@ -1144,7 +1144,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const existingChildren = mapRemainingChildren(returnFiber, oldFiber); // Keep scanning and use the map to restore deleted items as moves. - for (; !step.done; newIdx++, step = newChildren.next()) { + for (; !step.done; newIdx++, (step = newChildren.next())) { const newFiber = updateFromMap( existingChildren, returnFiber, @@ -1235,7 +1235,10 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { existing._debugOwner = element._owner; } return existing; - } else if (child.tag === Fragment && element.type === REACT_FRAGMENT_TYPE) { + } else if ( + child.tag === Fragment && + element.type === REACT_FRAGMENT_TYPE + ) { deleteRemainingChildren(returnFiber, child.sibling); const existing = useFiber(child, expirationTime); existing.ref = coerceRef(child, element); @@ -1404,7 +1407,9 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { return reconcileChildrenArray( returnFiber, currentFirstChild, - Array.isArray(newChild.props.children) ? newChild.props.children : [newChild.props.children], + Array.isArray(newChild.props.children) + ? newChild.props.children + : [newChild.props.children], expirationTime, ); } From 6df523d91d77ec379432906953a57d7fe4c9a177 Mon Sep 17 00:00:00 2001 From: Clement Hoang Date: Tue, 17 Oct 2017 14:26:37 -0700 Subject: [PATCH 14/43] Merge branches in updateElement --- src/renderers/shared/fiber/ReactChildFiber.js | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/src/renderers/shared/fiber/ReactChildFiber.js b/src/renderers/shared/fiber/ReactChildFiber.js index 0e5963130fdc7..5cd65a0900f07 100644 --- a/src/renderers/shared/fiber/ReactChildFiber.js +++ b/src/renderers/shared/fiber/ReactChildFiber.js @@ -383,20 +383,23 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { ): Fiber { if ( current !== null && - current.tag === Fragment && - element.type === REACT_FRAGMENT_TYPE + (current.type === element.type || + (current.tag === Fragment && element.type === REACT_FRAGMENT_TYPE)) ) { // Move based on index const existing = useFiber(current, expirationTime); existing.ref = coerceRef(current, element); - existing.pendingProps = element.props.children; + existing.pendingProps = current.tag === Fragment && + element.type === REACT_FRAGMENT_TYPE + ? element.props.children + : element.props; existing.return = returnFiber; if (__DEV__) { existing._debugSource = element._source; existing._debugOwner = element._owner; } return existing; - } else if (current === null || current.type !== element.type) { + } else { // Insert const created = createFiberFromElement( element, @@ -406,17 +409,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { created.ref = coerceRef(current, element); created.return = returnFiber; return created; - } else { - // Move based on index - const existing = useFiber(current, expirationTime); - existing.ref = coerceRef(current, element); - existing.pendingProps = element.props; - existing.return = returnFiber; - if (__DEV__) { - existing._debugSource = element._source; - existing._debugOwner = element._owner; - } - return existing; } } From b4f17f66e52317388d85aca19479722480441cf7 Mon Sep 17 00:00:00 2001 From: Clement Hoang Date: Tue, 17 Oct 2017 15:01:44 -0700 Subject: [PATCH 15/43] Remove unnecessary check --- src/renderers/shared/fiber/ReactChildFiber.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/renderers/shared/fiber/ReactChildFiber.js b/src/renderers/shared/fiber/ReactChildFiber.js index 5cd65a0900f07..0d896f27ce798 100644 --- a/src/renderers/shared/fiber/ReactChildFiber.js +++ b/src/renderers/shared/fiber/ReactChildFiber.js @@ -86,7 +86,6 @@ const isArray = Array.isArray; const { FunctionalComponent, ClassComponent, - HostRoot, HostText, HostPortal, CoroutineComponent, @@ -1388,13 +1387,9 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { if (isObject) { switch (newChild.$$typeof) { case REACT_ELEMENT_TYPE: - // If newChild is a top-level unkeyed fragment, don't create a fiber if ( newChild.type === REACT_FRAGMENT_TYPE && - newChild.key === null && - (returnFiber.tag === HostRoot || - returnFiber.tag === ClassComponent || - returnFiber.tag === FunctionalComponent) + newChild.key === null ) { return reconcileChildrenArray( returnFiber, From 6b374f8e76e83afc46b204c6fe2ba6627393197a Mon Sep 17 00:00:00 2001 From: Clement Hoang Date: Tue, 17 Oct 2017 16:29:15 -0700 Subject: [PATCH 16/43] Re-use createFiberFromFragment for fragment case --- src/renderers/shared/fiber/ReactChildFiber.js | 2 ++ src/renderers/shared/fiber/ReactFiber.js | 14 +++++++------- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/renderers/shared/fiber/ReactChildFiber.js b/src/renderers/shared/fiber/ReactChildFiber.js index 0d896f27ce798..434db824b62eb 100644 --- a/src/renderers/shared/fiber/ReactChildFiber.js +++ b/src/renderers/shared/fiber/ReactChildFiber.js @@ -502,6 +502,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { fragment, returnFiber.internalContextTag, expirationTime, + null, ); created.return = returnFiber; return created; @@ -582,6 +583,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { newChild, returnFiber.internalContextTag, expirationTime, + null, ); created.return = returnFiber; return created; diff --git a/src/renderers/shared/fiber/ReactFiber.js b/src/renderers/shared/fiber/ReactFiber.js index df3c38a5a34d0..aa5f7bcb95a82 100644 --- a/src/renderers/shared/fiber/ReactFiber.js +++ b/src/renderers/shared/fiber/ReactFiber.js @@ -319,9 +319,8 @@ exports.createFiberFromElement = function( fiber.type = type; fiber.pendingProps = element.props; } else if (type === REACT_FRAGMENT_TYPE) { - // special case for fragments, we create the fiber and return early - fiber = createFiber(Fragment, key, internalContextTag); - fiber.pendingProps = element.props.children; + // special case for fragments; create the fiber and return early + return createFiberFromFragment(element.props.children, internalContextTag, expirationTime, key); } else if (typeof type === 'string') { fiber = createFiber(HostComponent, key, internalContextTag); fiber.type = type; @@ -376,19 +375,20 @@ exports.createFiberFromElement = function( return fiber; }; -exports.createFiberFromFragment = function( +function createFiberFromFragment( elements: ReactFragment, internalContextTag: TypeOfInternalContext, expirationTime: ExpirationTime, + key: String | null, ): Fiber { - // TODO: Consider supporting keyed fragments. Technically, we accidentally - // support that in the existing React. - const fiber = createFiber(Fragment, null, internalContextTag); + const fiber = createFiber(Fragment, key, internalContextTag); fiber.pendingProps = elements; fiber.expirationTime = expirationTime; return fiber; }; +exports.createFiberFromFragment = createFiberFromFragment; + exports.createFiberFromText = function( content: string, internalContextTag: TypeOfInternalContext, From 7ceb63173957fbfdb06e0b6887d2a22d2afa86c1 Mon Sep 17 00:00:00 2001 From: Clement Hoang Date: Tue, 17 Oct 2017 17:00:15 -0700 Subject: [PATCH 17/43] Simplyify branches by adding type field to fragment fiber --- src/renderers/shared/fiber/ReactChildFiber.js | 32 ++++--------------- src/renderers/shared/fiber/ReactFiber.js | 12 +++++-- 2 files changed, 15 insertions(+), 29 deletions(-) diff --git a/src/renderers/shared/fiber/ReactChildFiber.js b/src/renderers/shared/fiber/ReactChildFiber.js index 434db824b62eb..70f31b7666966 100644 --- a/src/renderers/shared/fiber/ReactChildFiber.js +++ b/src/renderers/shared/fiber/ReactChildFiber.js @@ -380,16 +380,11 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { element: ReactElement, expirationTime: ExpirationTime, ): Fiber { - if ( - current !== null && - (current.type === element.type || - (current.tag === Fragment && element.type === REACT_FRAGMENT_TYPE)) - ) { + if (current !== null && current.type === element.type) { // Move based on index const existing = useFiber(current, expirationTime); existing.ref = coerceRef(current, element); - existing.pendingProps = current.tag === Fragment && - element.type === REACT_FRAGMENT_TYPE + existing.pendingProps = element.type === REACT_FRAGMENT_TYPE ? element.props.children : element.props; existing.return = returnFiber; @@ -1221,21 +1216,9 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { deleteRemainingChildren(returnFiber, child.sibling); const existing = useFiber(child, expirationTime); existing.ref = coerceRef(child, element); - existing.pendingProps = element.props; - existing.return = returnFiber; - if (__DEV__) { - existing._debugSource = element._source; - existing._debugOwner = element._owner; - } - return existing; - } else if ( - child.tag === Fragment && - element.type === REACT_FRAGMENT_TYPE - ) { - deleteRemainingChildren(returnFiber, child.sibling); - const existing = useFiber(child, expirationTime); - existing.ref = coerceRef(child, element); - existing.pendingProps = element.props.children; + existing.pendingProps = element.type === REACT_FRAGMENT_TYPE + ? element.props.children + : element.props; existing.return = returnFiber; if (__DEV__) { existing._debugSource = element._source; @@ -1389,10 +1372,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { if (isObject) { switch (newChild.$$typeof) { case REACT_ELEMENT_TYPE: - if ( - newChild.type === REACT_FRAGMENT_TYPE && - newChild.key === null - ) { + if (newChild.type === REACT_FRAGMENT_TYPE && newChild.key === null) { return reconcileChildrenArray( returnFiber, currentFirstChild, diff --git a/src/renderers/shared/fiber/ReactFiber.js b/src/renderers/shared/fiber/ReactFiber.js index aa5f7bcb95a82..baf2eb2092490 100644 --- a/src/renderers/shared/fiber/ReactFiber.js +++ b/src/renderers/shared/fiber/ReactFiber.js @@ -320,7 +320,12 @@ exports.createFiberFromElement = function( fiber.pendingProps = element.props; } else if (type === REACT_FRAGMENT_TYPE) { // special case for fragments; create the fiber and return early - return createFiberFromFragment(element.props.children, internalContextTag, expirationTime, key); + return createFiberFromFragment( + element.props.children, + internalContextTag, + expirationTime, + key, + ); } else if (typeof type === 'string') { fiber = createFiber(HostComponent, key, internalContextTag); fiber.type = type; @@ -379,13 +384,14 @@ function createFiberFromFragment( elements: ReactFragment, internalContextTag: TypeOfInternalContext, expirationTime: ExpirationTime, - key: String | null, + key: null | string, ): Fiber { const fiber = createFiber(Fragment, key, internalContextTag); + fiber.type = REACT_FRAGMENT_TYPE; fiber.pendingProps = elements; fiber.expirationTime = expirationTime; return fiber; -}; +} exports.createFiberFromFragment = createFiberFromFragment; From 53969d34526f47af87d8312f4edad42de4867803 Mon Sep 17 00:00:00 2001 From: Clement Hoang Date: Tue, 17 Oct 2017 17:57:26 -0700 Subject: [PATCH 18/43] Move branching logic for fragments to broader methods when possible. --- src/renderers/noop/ReactNoopEntry.js | 3 +- src/renderers/shared/fiber/ReactChildFiber.js | 59 +++++++++++++++++-- src/renderers/shared/fiber/ReactFiber.js | 15 ++--- 3 files changed, 63 insertions(+), 14 deletions(-) diff --git a/src/renderers/noop/ReactNoopEntry.js b/src/renderers/noop/ReactNoopEntry.js index 89bfc33c2d73e..7ec8d4e1911bf 100644 --- a/src/renderers/noop/ReactNoopEntry.js +++ b/src/renderers/noop/ReactNoopEntry.js @@ -433,7 +433,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/src/renderers/shared/fiber/ReactChildFiber.js b/src/renderers/shared/fiber/ReactChildFiber.js index 70f31b7666966..4cbe39eb6cd22 100644 --- a/src/renderers/shared/fiber/ReactChildFiber.js +++ b/src/renderers/shared/fiber/ReactChildFiber.js @@ -384,9 +384,10 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { // Move based on index const existing = useFiber(current, expirationTime); existing.ref = coerceRef(current, element); - existing.pendingProps = element.type === REACT_FRAGMENT_TYPE - ? element.props.children - : element.props; + // existing.pendingProps = element.type === REACT_FRAGMENT_TYPE + // ? element.props.children + // : element.props; + existing.pendingProps = element.props; existing.return = returnFiber; if (__DEV__) { existing._debugSource = element._source; @@ -490,6 +491,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { current: Fiber | null, fragment: Iterable<*>, expirationTime: ExpirationTime, + key: null | string, ): Fiber { if (current === null || current.tag !== Fragment) { // Insert @@ -497,7 +499,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { fragment, returnFiber.internalContextTag, expirationTime, - null, + key, ); created.return = returnFiber; return created; @@ -531,6 +533,18 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { if (typeof newChild === 'object' && newChild !== null) { switch (newChild.$$typeof) { case REACT_ELEMENT_TYPE: { + if (newChild.type === REACT_FRAGMENT_TYPE) { + const created = createFiberFromFragment( + isArray(newChild.props.children) + ? newChild.props.children + : [newChild.props.children], + returnFiber.internalContextTag, + expirationTime, + newChild.key, + ); + created.return = returnFiber; + return created; + } const created = createFiberFromElement( newChild, returnFiber.internalContextTag, @@ -625,6 +639,17 @@ 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, + isArray(newChild.props.children) + ? newChild.props.children + : [newChild.props.children], + expirationTime, + key, + ); + } return updateElement( returnFiber, oldFiber, @@ -680,7 +705,13 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { if (key !== null) { return null; } - return updateFragment(returnFiber, oldFiber, newChild, expirationTime); + return updateFragment( + returnFiber, + oldFiber, + newChild, + expirationTime, + null, + ); } throwOnInvalidObjectType(returnFiber, newChild); @@ -721,6 +752,17 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { existingChildren.get( newChild.key === null ? newIdx : newChild.key, ) || null; + if (newChild.type === REACT_FRAGMENT_TYPE) { + return updateFragment( + returnFiber, + matchedFiber, + isArray(newChild.props.children) + ? newChild.props.children + : [newChild.props.children], + expirationTime, + newChild.key, + ); + } return updateElement( returnFiber, matchedFiber, @@ -775,6 +817,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { matchedFiber, newChild, expirationTime, + null, ); } @@ -1372,16 +1415,20 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { if (isObject) { switch (newChild.$$typeof) { case REACT_ELEMENT_TYPE: + // is this right? if (newChild.type === REACT_FRAGMENT_TYPE && newChild.key === null) { return reconcileChildrenArray( returnFiber, currentFirstChild, - Array.isArray(newChild.props.children) + isArray(newChild.props.children) ? newChild.props.children : [newChild.props.children], expirationTime, ); } + // else if key is not null, reconcile wat SO CONFUSED + // AM I CREATING MORE FRAGMENT FIBERS I DON'T EVEN KNOW ANYMORE + return placeSingleChild( reconcileSingleElement( returnFiber, diff --git a/src/renderers/shared/fiber/ReactFiber.js b/src/renderers/shared/fiber/ReactFiber.js index baf2eb2092490..4f0c5a06b14bb 100644 --- a/src/renderers/shared/fiber/ReactFiber.js +++ b/src/renderers/shared/fiber/ReactFiber.js @@ -318,18 +318,19 @@ exports.createFiberFromElement = function( : 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 (type === REACT_FRAGMENT_TYPE) { - // special case for fragments; create the fiber and return early - return createFiberFromFragment( - element.props.children, + fiber = createFiberFromFragment( + Array.isArray(element.props.children) + ? element.props.children + : [element.props.children], internalContextTag, expirationTime, key, ); - } else if (typeof type === 'string') { - fiber = createFiber(HostComponent, key, internalContextTag); - fiber.type = type; - fiber.pendingProps = element.props; } else if ( typeof type === 'object' && type !== null && From 8ecb60c49c3e0afc700cabed755b9c29a26509e6 Mon Sep 17 00:00:00 2001 From: Clement Hoang Date: Wed, 18 Oct 2017 13:11:28 -0700 Subject: [PATCH 19/43] Add more tests for fragments --- .../class/__tests__/ReactFragment-test.js | 118 +++++++++++++++--- .../ReactJSXElementValidator-test.js | 32 +++-- src/renderers/shared/fiber/ReactChildFiber.js | 5 - 3 files changed, 123 insertions(+), 32 deletions(-) diff --git a/src/isomorphic/modern/class/__tests__/ReactFragment-test.js b/src/isomorphic/modern/class/__tests__/ReactFragment-test.js index 56d8154904f44..9c14e8f9a4b21 100644 --- a/src/isomorphic/modern/class/__tests__/ReactFragment-test.js +++ b/src/isomorphic/modern/class/__tests__/ReactFragment-test.js @@ -1,10 +1,8 @@ /** - * Copyright 2013-present, Facebook, Inc. - * All rights reserved. + * Copyright (c) 2013-present, Facebook, Inc. * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * 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 */ @@ -20,6 +18,14 @@ describe('ReactFragment', () => { ReactNoop = require('react-noop-renderer'); }); + function span(prop) { + return {type: 'span', children: [], prop}; + } + + function text(val) { + return {text: val}; + } + it('should render a single child via noop renderer', () => { const element = ( @@ -30,9 +36,7 @@ describe('ReactFragment', () => { ReactNoop.render(element); ReactNoop.flush(); - expect(ReactNoop.getChildren()).toEqual([ - {type: 'span', children: [], prop: undefined}, - ]); + expect(ReactNoop.getChildren()).toEqual([span()]); }); it('should render multiple children via noop renderer', () => { @@ -45,17 +49,16 @@ describe('ReactFragment', () => { ReactNoop.render(element); ReactNoop.flush(); - expect(ReactNoop.getChildren()).toEqual([ - {text: 'hello '}, - {type: 'span', children: [], prop: undefined}, - ]); + expect(ReactNoop.getChildren()).toEqual([text('hello '), span()]); }); - it('should preserve state of children', function() { + it('should preserve state of children with 1 level nesting', function() { var instance = null; + var ops = []; class Stateful extends React.Component { render() { + ops.push('Stateful render'); instance = this; return
Hello
; } @@ -63,9 +66,7 @@ describe('ReactFragment', () => { function Fragment({condition}) { return condition - ? - - + ? :
World
@@ -92,14 +93,17 @@ describe('ReactFragment', () => { var instanceB = instance; + expect(ops).toEqual(['Stateful render', 'Stateful render']); expect(instanceB).toBe(instanceA); }); - it('should not preserve state when switching to a nested fragment', function() { + it('should not preserve state of children with 2 levels nesting', function() { var instance = null; + var ops = []; class Stateful extends React.Component { render() { + ops.push('Stateful render'); instance = this; return
Hello
; } @@ -128,14 +132,17 @@ describe('ReactFragment', () => { var instanceB = instance; + expect(ops).toEqual(['Stateful render', 'Stateful render']); expect(instanceB).not.toBe(instanceA); }); - it('should preserve state in a reorder', function() { + it('should preserve state of children in a reorder', function() { var instance = null; + var ops = []; class Stateful extends React.Component { render() { + ops.push('Stateful render'); instance = this; return
Hello
; } @@ -169,6 +176,81 @@ describe('ReactFragment', () => { var instanceB = instance; + expect(ops).toEqual(['Stateful render', 'Stateful render']); + expect(instanceB).toBe(instanceA); + }); + + it('should preserve state of children when the keys are same', function() { + var instance = null; + var ops = []; + + class Stateful extends React.Component { + render() { + ops.push('Stateful render'); + instance = this; + return
Hello
; + } + } + + function Fragment({condition}) { + return condition + ? + + + : + + World + ; + } + + ReactNoop.render(); + ReactNoop.flush(); + + var instanceA = instance; + + ReactNoop.render(); + ReactNoop.flush(); + + var instanceB = instance; + + expect(ops).toEqual(['Stateful render', 'Stateful render']); expect(instanceB).toBe(instanceA); }); + + it('should not preserve state of children when the keys are different', function() { + var instance = null; + var ops = []; + + class Stateful extends React.Component { + render() { + ops.push('Stateful render'); + instance = this; + return
Hello
; + } + } + + function Fragment({condition}) { + return condition + ? + + + : + + World + ; + } + + ReactNoop.render(); + ReactNoop.flush(); + + var instanceA = instance; + + ReactNoop.render(); + ReactNoop.flush(); + + var instanceB = instance; + + expect(ops).toEqual(['Stateful render', 'Stateful render']); + expect(instanceB).not.toBe(instanceA); + }); }); diff --git a/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js b/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js index 8ac0fafdae176..71527ec711630 100644 --- a/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js +++ b/src/isomorphic/modern/element/__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 ReactDOMFeatureFlags = require('ReactDOMFeatureFlags'); var React; var ReactDOM; var ReactTestUtils; @@ -112,18 +111,33 @@ describe('ReactJSXElementValidator', () => { it('does not warns for fragments of multiple elements without keys', () => { spyOn(console, 'error'); - if (ReactDOMFeatureFlags.useFiber) { - ReactTestUtils.renderIntoDocument( - - 1 - 2 - , - ); - } + ReactTestUtils.renderIntoDocument( + + 1 + 2 + , + ); expectDev(console.error.calls.count()).toBe(0); }); + it('warns for fragments of multiple elements with same key', () => { + spyOn(console, 'error'); + + 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', + ); + }); + it('does not warns for arrays of elements with keys', () => { spyOn(console, 'error'); diff --git a/src/renderers/shared/fiber/ReactChildFiber.js b/src/renderers/shared/fiber/ReactChildFiber.js index 4cbe39eb6cd22..57480c6ef9f17 100644 --- a/src/renderers/shared/fiber/ReactChildFiber.js +++ b/src/renderers/shared/fiber/ReactChildFiber.js @@ -384,9 +384,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { // Move based on index const existing = useFiber(current, expirationTime); existing.ref = coerceRef(current, element); - // existing.pendingProps = element.type === REACT_FRAGMENT_TYPE - // ? element.props.children - // : element.props; existing.pendingProps = element.props; existing.return = returnFiber; if (__DEV__) { @@ -1426,8 +1423,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { expirationTime, ); } - // else if key is not null, reconcile wat SO CONFUSED - // AM I CREATING MORE FRAGMENT FIBERS I DON'T EVEN KNOW ANYMORE return placeSingleChild( reconcileSingleElement( From ab1a58ec0675b34d6eb15854c4b573f2abebe489 Mon Sep 17 00:00:00 2001 From: Clement Hoang Date: Wed, 18 Oct 2017 15:12:36 -0700 Subject: [PATCH 20/43] Address Dan's feedback --- .../ReactJSXElementValidator-test.js | 24 +++++-------------- src/renderers/shared/fiber/ReactChildFiber.js | 1 - 2 files changed, 6 insertions(+), 19 deletions(-) diff --git a/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js b/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js index 71527ec711630..4ea2592d1d82c 100644 --- a/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js +++ b/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js @@ -108,17 +108,13 @@ describe('ReactJSXElementValidator', () => { ); }); - it('does not warns for fragments of multiple elements without keys', () => { - spyOn(console, 'error'); - + it('does not warn for fragments of multiple elements without keys', () => { ReactTestUtils.renderIntoDocument( 1 2 , ); - - expectDev(console.error.calls.count()).toBe(0); }); it('warns for fragments of multiple elements with same key', () => { @@ -134,11 +130,11 @@ describe('ReactJSXElementValidator', () => { expectDev(console.error.calls.count()).toBe(1); expectDev(console.error.calls.argsFor(0)[0]).toContain( - 'Encountered two children with the same key', + 'Encountered two children with the same key, `a`.', ); }); - it('does not warns for arrays of elements with keys', () => { + it('does not warn for arrays of elements with keys', () => { spyOn(console, 'error'); ReactTestUtils.renderIntoDocument( @@ -148,7 +144,7 @@ describe('ReactJSXElementValidator', () => { 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 = { @@ -193,24 +189,16 @@ describe('ReactJSXElementValidator', () => { }); it('does not warn when the element is directly as children', () => { - spyOn(console, 'error'); - - void ( + ReactTestUtils.renderIntoDocument( - + , ); - - expectDev(console.error.calls.count()).toBe(0); }); 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.', () => { diff --git a/src/renderers/shared/fiber/ReactChildFiber.js b/src/renderers/shared/fiber/ReactChildFiber.js index 57480c6ef9f17..091821fdfd15e 100644 --- a/src/renderers/shared/fiber/ReactChildFiber.js +++ b/src/renderers/shared/fiber/ReactChildFiber.js @@ -1412,7 +1412,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { if (isObject) { switch (newChild.$$typeof) { case REACT_ELEMENT_TYPE: - // is this right? if (newChild.type === REACT_FRAGMENT_TYPE && newChild.key === null) { return reconcileChildrenArray( returnFiber, From b7fff43fc94a592dee3351bbccd79fe1387a16d3 Mon Sep 17 00:00:00 2001 From: Clement Hoang Date: Wed, 18 Oct 2017 18:52:21 -0700 Subject: [PATCH 21/43] Move REACT_FRAGMENT_TYPE into __DEV__ block for DCE --- .../classic/element/ReactElementValidator.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/isomorphic/classic/element/ReactElementValidator.js b/src/isomorphic/classic/element/ReactElementValidator.js index ea0b00e807c23..983901608f7b9 100644 --- a/src/isomorphic/classic/element/ReactElementValidator.js +++ b/src/isomorphic/classic/element/ReactElementValidator.js @@ -19,12 +19,6 @@ var ReactCurrentOwner = require('ReactCurrentOwner'); var ReactElement = require('ReactElement'); -const REACT_FRAGMENT_TYPE = - (typeof Symbol === 'function' && - Symbol.for && - Symbol.for('react.fragment')) || - 0xad9c; - if (__DEV__) { var checkPropTypes = require('prop-types/checkPropTypes'); var lowPriorityWarning = require('lowPriorityWarning'); @@ -61,6 +55,12 @@ if (__DEV__) { stack += ReactDebugCurrentFrame.getStackAddendum() || ''; return stack; }; + + var REACT_FRAGMENT_TYPE = + (typeof Symbol === 'function' && + Symbol.for && + Symbol.for('react.fragment')) || + 0xad9c; } var ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator; From a6aca2895b8a3d2bd87af85ee45f4c8c34dc7830 Mon Sep 17 00:00:00 2001 From: Clement Hoang Date: Wed, 18 Oct 2017 19:38:23 -0700 Subject: [PATCH 22/43] Change hex representation of REACT_FRAGMENT_TYPE to follow convention --- src/isomorphic/ReactEntry.js | 2 +- src/isomorphic/classic/element/ReactElementValidator.js | 2 +- src/renderers/shared/fiber/ReactChildFiber.js | 2 +- src/renderers/shared/fiber/ReactFiber.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/isomorphic/ReactEntry.js b/src/isomorphic/ReactEntry.js index 58881c256a9de..ed49642035958 100644 --- a/src/isomorphic/ReactEntry.js +++ b/src/isomorphic/ReactEntry.js @@ -31,7 +31,7 @@ const REACT_FRAGMENT_TYPE = (typeof Symbol === 'function' && Symbol.for && Symbol.for('react.fragment')) || - 0xad9c; + 0xeacb; var React = { Children: { diff --git a/src/isomorphic/classic/element/ReactElementValidator.js b/src/isomorphic/classic/element/ReactElementValidator.js index 983901608f7b9..32da1878d1758 100644 --- a/src/isomorphic/classic/element/ReactElementValidator.js +++ b/src/isomorphic/classic/element/ReactElementValidator.js @@ -60,7 +60,7 @@ if (__DEV__) { (typeof Symbol === 'function' && Symbol.for && Symbol.for('react.fragment')) || - 0xad9c; + 0xeacb; } var ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator; diff --git a/src/renderers/shared/fiber/ReactChildFiber.js b/src/renderers/shared/fiber/ReactChildFiber.js index 091821fdfd15e..086844578ffca 100644 --- a/src/renderers/shared/fiber/ReactChildFiber.js +++ b/src/renderers/shared/fiber/ReactChildFiber.js @@ -106,7 +106,7 @@ const REACT_FRAGMENT_TYPE = (typeof Symbol === 'function' && Symbol.for && Symbol.for('react.fragment')) || - 0xad9c; + 0xeacb; function getIteratorFn(maybeIterable: ?any): ?() => ?Iterator<*> { if (maybeIterable === null || typeof maybeIterable === 'undefined') { diff --git a/src/renderers/shared/fiber/ReactFiber.js b/src/renderers/shared/fiber/ReactFiber.js index 4f0c5a06b14bb..036074c1b17ed 100644 --- a/src/renderers/shared/fiber/ReactFiber.js +++ b/src/renderers/shared/fiber/ReactFiber.js @@ -47,7 +47,7 @@ const REACT_FRAGMENT_TYPE = (typeof Symbol === 'function' && Symbol.for && Symbol.for('react.fragment')) || - 0xad9c; + 0xeacb; if (__DEV__) { var getComponentName = require('getComponentName'); From 372a62afb4faa23d486c8f47c473db76228ba679 Mon Sep 17 00:00:00 2001 From: Clement Hoang Date: Thu, 19 Oct 2017 15:05:43 -0700 Subject: [PATCH 23/43] Remove unnecessary branching and isArray checks --- .../class/__tests__/ReactFragment-test.js | 42 ++++++++++++- src/renderers/shared/fiber/ReactChildFiber.js | 60 +++++++------------ src/renderers/shared/fiber/ReactFiber.js | 4 +- 3 files changed, 63 insertions(+), 43 deletions(-) diff --git a/src/isomorphic/modern/class/__tests__/ReactFragment-test.js b/src/isomorphic/modern/class/__tests__/ReactFragment-test.js index 9c14e8f9a4b21..32128274d7b4c 100644 --- a/src/isomorphic/modern/class/__tests__/ReactFragment-test.js +++ b/src/isomorphic/modern/class/__tests__/ReactFragment-test.js @@ -97,7 +97,44 @@ describe('ReactFragment', () => { expect(instanceB).toBe(instanceA); }); - it('should not preserve state of children with 2 levels nesting', function() { + it('should not preserve state of children if no siblings and nested', function() { + var instance = null; + var ops = []; + + class Stateful extends React.Component { + render() { + ops.push('Stateful render'); + instance = this; + return
Hello
; + } + } + + function Fragment({condition}) { + return condition + ? + : + + + + ; + } + ReactNoop.render(); + ReactNoop.flush(); + + var instanceA = instance; + + expect(instanceA).not.toBe(null); + + ReactNoop.render(); + ReactNoop.flush(); + + var instanceB = instance; + + expect(ops).toEqual(['Stateful render', 'Stateful render']); + expect(instanceB).not.toBe(instanceA); + }); + + it('should not preserve state of children if nested with siblings', function() { var instance = null; var ops = []; @@ -115,9 +152,8 @@ describe('ReactFragment', () => { : -
World
-
+
; } ReactNoop.render(); diff --git a/src/renderers/shared/fiber/ReactChildFiber.js b/src/renderers/shared/fiber/ReactChildFiber.js index 086844578ffca..694a809dbeef2 100644 --- a/src/renderers/shared/fiber/ReactChildFiber.js +++ b/src/renderers/shared/fiber/ReactChildFiber.js @@ -530,18 +530,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { if (typeof newChild === 'object' && newChild !== null) { switch (newChild.$$typeof) { case REACT_ELEMENT_TYPE: { - if (newChild.type === REACT_FRAGMENT_TYPE) { - const created = createFiberFromFragment( - isArray(newChild.props.children) - ? newChild.props.children - : [newChild.props.children], - returnFiber.internalContextTag, - expirationTime, - newChild.key, - ); - created.return = returnFiber; - return created; - } const created = createFiberFromElement( newChild, returnFiber.internalContextTag, @@ -635,18 +623,16 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { if (typeof newChild === 'object' && newChild !== null) { switch (newChild.$$typeof) { case REACT_ELEMENT_TYPE: { + if (newChild.type === REACT_FRAGMENT_TYPE) { + return updateSlot( + returnFiber, + oldFiber, + newChild.props.children, + expirationTime, + ); + } + if (newChild.key === key) { - if (newChild.type === REACT_FRAGMENT_TYPE) { - return updateFragment( - returnFiber, - oldFiber, - isArray(newChild.props.children) - ? newChild.props.children - : [newChild.props.children], - expirationTime, - key, - ); - } return updateElement( returnFiber, oldFiber, @@ -697,17 +683,12 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { } if (isArray(newChild) || getIteratorFn(newChild)) { - // Fragments doesn'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, - null, + key, ); } @@ -753,9 +734,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { return updateFragment( returnFiber, matchedFiber, - isArray(newChild.props.children) - ? newChild.props.children - : [newChild.props.children], + newChild.props.children, expirationTime, newChild.key, ); @@ -1401,6 +1380,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { currentFirstChild: Fiber | null, newChild: any, expirationTime: ExpirationTime, + nested?: boolean, ): Fiber | null { // This function is not recursive. // If the top level item is an array, we treat it as a set of children, @@ -1412,14 +1392,20 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { if (isObject) { switch (newChild.$$typeof) { case REACT_ELEMENT_TYPE: - if (newChild.type === REACT_FRAGMENT_TYPE && newChild.key === null) { - return reconcileChildrenArray( + // This function recurses only on a top-level fragment, + // so that it is treated as a set of children + // Otherwise, we follow the normal flow. + if ( + newChild.type === REACT_FRAGMENT_TYPE && + newChild.key === null && + !nested + ) { + return reconcileChildFibers( returnFiber, currentFirstChild, - isArray(newChild.props.children) - ? newChild.props.children - : [newChild.props.children], + newChild.props.children, expirationTime, + true, ); } diff --git a/src/renderers/shared/fiber/ReactFiber.js b/src/renderers/shared/fiber/ReactFiber.js index 036074c1b17ed..b3af9eb089396 100644 --- a/src/renderers/shared/fiber/ReactFiber.js +++ b/src/renderers/shared/fiber/ReactFiber.js @@ -324,9 +324,7 @@ exports.createFiberFromElement = function( fiber.pendingProps = element.props; } else if (type === REACT_FRAGMENT_TYPE) { fiber = createFiberFromFragment( - Array.isArray(element.props.children) - ? element.props.children - : [element.props.children], + element.props.children, internalContextTag, expirationTime, key, From 84054657f90bdef0a8ab4b7734d414be1c835ac2 Mon Sep 17 00:00:00 2001 From: Clement Hoang Date: Thu, 19 Oct 2017 18:14:42 -0700 Subject: [PATCH 24/43] Update test for preserving children state when keys are same --- src/isomorphic/modern/class/__tests__/ReactFragment-test.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/isomorphic/modern/class/__tests__/ReactFragment-test.js b/src/isomorphic/modern/class/__tests__/ReactFragment-test.js index 32128274d7b4c..55be25085d921 100644 --- a/src/isomorphic/modern/class/__tests__/ReactFragment-test.js +++ b/src/isomorphic/modern/class/__tests__/ReactFragment-test.js @@ -187,13 +187,14 @@ describe('ReactFragment', () => { function Fragment({condition}) { return condition ? - +
+
World
: - +
World
@@ -235,6 +236,7 @@ describe('ReactFragment', () => {
: +
World ; } From cee245c24daffd2c8e8eed0ed1fc130200a3d5fd Mon Sep 17 00:00:00 2001 From: Clement Hoang Date: Fri, 20 Oct 2017 04:09:07 -0700 Subject: [PATCH 25/43] Fix updateSlot bug and add more tests --- .../class/__tests__/ReactFragment-test.js | 103 ++++++++++++++++++ src/renderers/shared/fiber/ReactChildFiber.js | 18 +-- 2 files changed, 112 insertions(+), 9 deletions(-) diff --git a/src/isomorphic/modern/class/__tests__/ReactFragment-test.js b/src/isomorphic/modern/class/__tests__/ReactFragment-test.js index 55be25085d921..b07d22d52fe38 100644 --- a/src/isomorphic/modern/class/__tests__/ReactFragment-test.js +++ b/src/isomorphic/modern/class/__tests__/ReactFragment-test.js @@ -52,6 +52,19 @@ describe('ReactFragment', () => { 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 instance = null; var ops = []; @@ -291,4 +304,94 @@ describe('ReactFragment', () => { expect(ops).toEqual(['Stateful render', 'Stateful render']); expect(instanceB).not.toBe(instanceA); }); + + it('should preserve state with reordering in multiple levels', function() { + var ops = []; + + class Stateful extends React.Component { + shouldComponentUpdate() { + ops.push('update'); + return true; + } + + render() { + return
Hello
; + } + } + + function Fragment({condition}) { + return condition + ?
+ + foo +
+ +
+
+ boop +
+ :
+ beep + +
+ +
+ bar +
+
; + } + + ReactNoop.render(); + ReactNoop.flush(); + + ReactNoop.render(); + ReactNoop.flush(); + + expect(ops).toEqual(['update']); + }); + + it('should preserve state with reordering in multiple levels', function() { + var ops = []; + + class Stateful extends React.Component { + shouldComponentUpdate() { + ops.push('update'); + return true; + } + + render() { + return
Hello
; + } + } + + function Fragment({condition}) { + return condition + ?
+ + foo +
+ +
+
+ boop +
+ :
+ beep + +
+ +
+ bar +
+
; + } + + ReactNoop.render(); + ReactNoop.flush(); + + ReactNoop.render(); + ReactNoop.flush(); + + expect(ops).toEqual(['update']); + }); }); diff --git a/src/renderers/shared/fiber/ReactChildFiber.js b/src/renderers/shared/fiber/ReactChildFiber.js index 694a809dbeef2..266c32022525b 100644 --- a/src/renderers/shared/fiber/ReactChildFiber.js +++ b/src/renderers/shared/fiber/ReactChildFiber.js @@ -623,16 +623,16 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { if (typeof newChild === 'object' && newChild !== null) { switch (newChild.$$typeof) { case REACT_ELEMENT_TYPE: { - if (newChild.type === REACT_FRAGMENT_TYPE) { - return updateSlot( - returnFiber, - oldFiber, - newChild.props.children, - expirationTime, - ); - } - if (newChild.key === key) { + if (newChild.type === REACT_FRAGMENT_TYPE) { + return updateFragment( + returnFiber, + oldFiber, + newChild.props.children, + expirationTime, + key, + ); + } return updateElement( returnFiber, oldFiber, From b3bac190cf96ed7af1854338c82f8ed62a3f57e7 Mon Sep 17 00:00:00 2001 From: Clement Hoang Date: Fri, 20 Oct 2017 04:18:16 -0700 Subject: [PATCH 26/43] Make fragment tests more robust by using ops pattern --- .../class/__tests__/ReactFragment-test.js | 104 ++++++------------ 1 file changed, 36 insertions(+), 68 deletions(-) diff --git a/src/isomorphic/modern/class/__tests__/ReactFragment-test.js b/src/isomorphic/modern/class/__tests__/ReactFragment-test.js index b07d22d52fe38..ef63187392b63 100644 --- a/src/isomorphic/modern/class/__tests__/ReactFragment-test.js +++ b/src/isomorphic/modern/class/__tests__/ReactFragment-test.js @@ -66,13 +66,14 @@ describe('ReactFragment', () => { }); it('should preserve state of children with 1 level nesting', function() { - var instance = null; var ops = []; class Stateful extends React.Component { + componentDidUpdate() { + ops.push('Update Stateful'); + } + render() { - ops.push('Stateful render'); - instance = this; return
Hello
; } } @@ -94,8 +95,6 @@ describe('ReactFragment', () => { ); ReactNoop.flush(); - var instanceA = instance; - ReactNoop.render(
@@ -104,20 +103,18 @@ describe('ReactFragment', () => { ); ReactNoop.flush(); - var instanceB = instance; - - expect(ops).toEqual(['Stateful render', 'Stateful render']); - expect(instanceB).toBe(instanceA); + expect(ops).toEqual(['Update Stateful']); }); it('should not preserve state of children if no siblings and nested', function() { - var instance = null; var ops = []; class Stateful extends React.Component { + componentDidUpdate() { + ops.push('Update Stateful'); + } + render() { - ops.push('Stateful render'); - instance = this; return
Hello
; } } @@ -134,27 +131,21 @@ describe('ReactFragment', () => { ReactNoop.render(); ReactNoop.flush(); - var instanceA = instance; - - expect(instanceA).not.toBe(null); - ReactNoop.render(); ReactNoop.flush(); - var instanceB = instance; - - expect(ops).toEqual(['Stateful render', 'Stateful render']); - expect(instanceB).not.toBe(instanceA); + expect(ops).toEqual([]); }); it('should not preserve state of children if nested with siblings', function() { - var instance = null; var ops = []; class Stateful extends React.Component { + componentDidUpdate() { + ops.push('Update Stateful'); + } + render() { - ops.push('Stateful render'); - instance = this; return
Hello
; } } @@ -172,27 +163,21 @@ describe('ReactFragment', () => { ReactNoop.render(); ReactNoop.flush(); - var instanceA = instance; - - expect(instanceA).not.toBe(null); - ReactNoop.render(); ReactNoop.flush(); - var instanceB = instance; - - expect(ops).toEqual(['Stateful render', 'Stateful render']); - expect(instanceB).not.toBe(instanceA); + expect(ops).toEqual([]); }); it('should preserve state of children in a reorder', function() { - var instance = null; var ops = []; class Stateful extends React.Component { + componentDidUpdate() { + ops.push('Update Stateful'); + } + render() { - ops.push('Stateful render'); - instance = this; return
Hello
; } } @@ -217,27 +202,21 @@ describe('ReactFragment', () => { ReactNoop.render(); ReactNoop.flush(); - var instanceA = instance; - - expect(instanceA).not.toBe(null); - ReactNoop.render(); ReactNoop.flush(); - var instanceB = instance; - - expect(ops).toEqual(['Stateful render', 'Stateful render']); - expect(instanceB).toBe(instanceA); + expect(ops).toEqual(['Update Stateful']); }); it('should preserve state of children when the keys are same', function() { - var instance = null; var ops = []; class Stateful extends React.Component { + componentDidUpdate() { + ops.push('Update Stateful'); + } + render() { - ops.push('Stateful render'); - instance = this; return
Hello
; } } @@ -257,25 +236,21 @@ describe('ReactFragment', () => { ReactNoop.render(); ReactNoop.flush(); - var instanceA = instance; - ReactNoop.render(); ReactNoop.flush(); - var instanceB = instance; - - expect(ops).toEqual(['Stateful render', 'Stateful render']); - expect(instanceB).toBe(instanceA); + expect(ops).toEqual(['Update Stateful']); }); it('should not preserve state of children when the keys are different', function() { - var instance = null; var ops = []; class Stateful extends React.Component { + componentDidUpdate() { + ops.push('Update Stateful'); + } + render() { - ops.push('Stateful render'); - instance = this; return
Hello
; } } @@ -294,24 +269,18 @@ describe('ReactFragment', () => { ReactNoop.render(); ReactNoop.flush(); - var instanceA = instance; - ReactNoop.render(); ReactNoop.flush(); - var instanceB = instance; - - expect(ops).toEqual(['Stateful render', 'Stateful render']); - expect(instanceB).not.toBe(instanceA); + expect(ops).toEqual([]); }); it('should preserve state with reordering in multiple levels', function() { var ops = []; class Stateful extends React.Component { - shouldComponentUpdate() { - ops.push('update'); - return true; + componentDidUpdate() { + ops.push('Update Stateful'); } render() { @@ -347,16 +316,15 @@ describe('ReactFragment', () => { ReactNoop.render(); ReactNoop.flush(); - expect(ops).toEqual(['update']); + expect(ops).toEqual(['Update Stateful']); }); it('should preserve state with reordering in multiple levels', function() { var ops = []; class Stateful extends React.Component { - shouldComponentUpdate() { - ops.push('update'); - return true; + componentDidUpdate() { + ops.push('Update Stateful'); } render() { @@ -392,6 +360,6 @@ describe('ReactFragment', () => { ReactNoop.render(); ReactNoop.flush(); - expect(ops).toEqual(['update']); + expect(ops).toEqual(['Update Stateful']); }); }); From 198ad8cadcad864d99ec35a5a831f1ba52afa0e7 Mon Sep 17 00:00:00 2001 From: Clement Hoang Date: Fri, 20 Oct 2017 04:33:03 -0700 Subject: [PATCH 27/43] Update jsx element validator to allow numbers and symbols --- .../classic/element/ReactElementValidator.js | 9 ++------- .../element/__tests__/ReactElementValidator-test.js | 12 +++--------- .../__tests__/ReactJSXElementValidator-test.js | 12 ++---------- 3 files changed, 7 insertions(+), 26 deletions(-) diff --git a/src/isomorphic/classic/element/ReactElementValidator.js b/src/isomorphic/classic/element/ReactElementValidator.js index 32da1878d1758..af9490c6d1898 100644 --- a/src/isomorphic/classic/element/ReactElementValidator.js +++ b/src/isomorphic/classic/element/ReactElementValidator.js @@ -55,12 +55,6 @@ if (__DEV__) { stack += ReactDebugCurrentFrame.getStackAddendum() || ''; return stack; }; - - var REACT_FRAGMENT_TYPE = - (typeof Symbol === 'function' && - Symbol.for && - Symbol.for('react.fragment')) || - 0xeacb; } var ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator; @@ -238,7 +232,8 @@ var ReactElementValidator = { var validType = typeof type === 'string' || typeof type === 'function' || - type === REACT_FRAGMENT_TYPE; + 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) { diff --git a/src/isomorphic/classic/element/__tests__/ReactElementValidator-test.js b/src/isomorphic/classic/element/__tests__/ReactElementValidator-test.js index 273669258ce86..a376e061d0ea4 100644 --- a/src/isomorphic/classic/element/__tests__/ReactElementValidator-test.js +++ b/src/isomorphic/classic/element/__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/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js b/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js index 4ea2592d1d82c..c86ee54961bef 100644 --- a/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js +++ b/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js @@ -270,14 +270,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 ' + @@ -297,14 +295,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', () => { From 1a47984f034bf5f8a4801a397e636e04d75a08ad Mon Sep 17 00:00:00 2001 From: Clement Hoang Date: Fri, 20 Oct 2017 11:05:55 -0700 Subject: [PATCH 28/43] Remove type field from fragment fiber --- src/renderers/shared/fiber/ReactChildFiber.js | 17 ++++++++++++++--- src/renderers/shared/fiber/ReactFiber.js | 1 - 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/renderers/shared/fiber/ReactChildFiber.js b/src/renderers/shared/fiber/ReactChildFiber.js index 266c32022525b..f9b3e3056c71d 100644 --- a/src/renderers/shared/fiber/ReactChildFiber.js +++ b/src/renderers/shared/fiber/ReactChildFiber.js @@ -380,11 +380,18 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { element: ReactElement, expirationTime: ExpirationTime, ): Fiber { - if (current !== null && current.type === element.type) { + // TODO: Split these into branches based on typeof type + if ( + current !== null && + (current.type === element.type || + (current.tag === Fragment && element.type === REACT_FRAGMENT_TYPE)) + ) { // Move based on index const existing = useFiber(current, expirationTime); existing.ref = coerceRef(current, 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; @@ -1231,7 +1238,11 @@ 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) { + // TODO: Split these into branches based on typeof type + if ( + child.type === element.type || + (child.tag === Fragment && element.type === REACT_FRAGMENT_TYPE) + ) { deleteRemainingChildren(returnFiber, child.sibling); const existing = useFiber(child, expirationTime); existing.ref = coerceRef(child, element); diff --git a/src/renderers/shared/fiber/ReactFiber.js b/src/renderers/shared/fiber/ReactFiber.js index b3af9eb089396..a87a5b9d57f3a 100644 --- a/src/renderers/shared/fiber/ReactFiber.js +++ b/src/renderers/shared/fiber/ReactFiber.js @@ -386,7 +386,6 @@ function createFiberFromFragment( key: null | string, ): Fiber { const fiber = createFiber(Fragment, key, internalContextTag); - fiber.type = REACT_FRAGMENT_TYPE; fiber.pendingProps = elements; fiber.expirationTime = expirationTime; return fiber; From 5edee97933ca74ac5c9777e26cc6c104b010ea96 Mon Sep 17 00:00:00 2001 From: Clement Hoang Date: Fri, 20 Oct 2017 11:14:43 -0700 Subject: [PATCH 29/43] Fork reconcileChildFibers instead of recursing --- src/renderers/shared/fiber/ReactChildFiber.js | 144 ++++++++++++++++-- 1 file changed, 133 insertions(+), 11 deletions(-) diff --git a/src/renderers/shared/fiber/ReactChildFiber.js b/src/renderers/shared/fiber/ReactChildFiber.js index f9b3e3056c71d..4bbbb02632d18 100644 --- a/src/renderers/shared/fiber/ReactChildFiber.js +++ b/src/renderers/shared/fiber/ReactChildFiber.js @@ -1383,15 +1383,145 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { return created; } + // A fork of reconcileChildFibers to be used when reconcileChildFibers + // encounters a top level fragment to treat it as a set of children. + // This function is not recursive. + function reconcileChildFibersForTopLevelFragment( + returnFiber: Fiber, + currentFirstChild: Fiber | null, + newChild: any, + expirationTime: ExpirationTime, + ): Fiber | null { + // This function is not recursive. + // If the top level item is an array, we treat it as a set of children, + // not as a fragment. Nested arrays on the other hand will be treated as + // fragment nodes. Recursion happens at the normal flow. + + // Handle object types + const isObject = typeof newChild === 'object' && newChild !== null; + if (isObject) { + switch (newChild.$$typeof) { + case REACT_ELEMENT_TYPE: + return placeSingleChild( + reconcileSingleElement( + returnFiber, + currentFirstChild, + newChild, + expirationTime, + ), + ); + case REACT_COROUTINE_TYPE: + return placeSingleChild( + reconcileSingleCoroutine( + returnFiber, + currentFirstChild, + newChild, + expirationTime, + ), + ); + case REACT_YIELD_TYPE: + return placeSingleChild( + reconcileSingleYield( + returnFiber, + currentFirstChild, + newChild, + expirationTime, + ), + ); + case REACT_PORTAL_TYPE: + return placeSingleChild( + reconcileSinglePortal( + returnFiber, + currentFirstChild, + newChild, + expirationTime, + ), + ); + } + } + + if (typeof newChild === 'string' || typeof newChild === 'number') { + return placeSingleChild( + reconcileSingleTextNode( + returnFiber, + currentFirstChild, + '' + newChild, + expirationTime, + ), + ); + } + + if (isArray(newChild)) { + return reconcileChildrenArray( + returnFiber, + currentFirstChild, + newChild, + expirationTime, + ); + } + + if (getIteratorFn(newChild)) { + return reconcileChildrenIterator( + returnFiber, + currentFirstChild, + newChild, + expirationTime, + ); + } + + if (isObject) { + throwOnInvalidObjectType(returnFiber, newChild); + } + + if (__DEV__) { + if (typeof newChild === 'function') { + warnOnFunctionType(); + } + } + if (typeof newChild === 'undefined') { + // If the new child is undefined, and the return fiber is a composite + // component, throw an error. If Fiber return types are disabled, + // we already threw above. + switch (returnFiber.tag) { + case ClassComponent: { + if (__DEV__) { + const instance = returnFiber.stateNode; + if (instance.render._isMockFunction) { + // We allow auto-mocks to proceed as if they're returning null. + break; + } + } + } + // Intentionally fall through to the next case, which handles both + // functions and classes + // eslint-disable-next-lined no-fallthrough + case FunctionalComponent: { + const Component = returnFiber.type; + invariant( + false, + '%s(...): Nothing was returned from render. This usually means a ' + + 'return statement is missing. Or, to render nothing, ' + + 'return null.', + Component.displayName || Component.name || 'Component', + ); + } + } + } + + // Remaining cases are all treated as empty. + return deleteRemainingChildren(returnFiber, currentFirstChild); + } + // This API will tag the children with the side-effect of the reconciliation // itself. They will be added to the side-effect list as we pass through the // children and the parent. + // This function is forked at reconcileChildFibersForTopLevelFragment + // Please reflect changes in this function to its fork as well function reconcileChildFibers( returnFiber: Fiber, currentFirstChild: Fiber | null, newChild: any, expirationTime: ExpirationTime, - nested?: boolean, ): Fiber | null { // This function is not recursive. // If the top level item is an array, we treat it as a set of children, @@ -1406,20 +1536,14 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { // This function recurses only on a top-level fragment, // so that it is treated as a set of children // Otherwise, we follow the normal flow. - if ( - newChild.type === REACT_FRAGMENT_TYPE && - newChild.key === null && - !nested - ) { - return reconcileChildFibers( + if (newChild.type === REACT_FRAGMENT_TYPE && newChild.key === null) { + return reconcileChildFibersForTopLevelFragment( returnFiber, currentFirstChild, newChild.props.children, expirationTime, - true, ); } - return placeSingleChild( reconcileSingleElement( returnFiber, @@ -1428,7 +1552,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { expirationTime, ), ); - case REACT_COROUTINE_TYPE: return placeSingleChild( reconcileSingleCoroutine( @@ -1447,7 +1570,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { expirationTime, ), ); - case REACT_PORTAL_TYPE: return placeSingleChild( reconcileSinglePortal( From 59f6828e79c7e626f06bb7131f360a9d562f3a60 Mon Sep 17 00:00:00 2001 From: Clement Hoang Date: Tue, 24 Oct 2017 17:51:46 -0700 Subject: [PATCH 30/43] Use ternary if condition --- packages/react-reconciler/src/ReactChildFiber.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/react-reconciler/src/ReactChildFiber.js b/packages/react-reconciler/src/ReactChildFiber.js index 8de694951d7ad..109730b7d8eb7 100644 --- a/packages/react-reconciler/src/ReactChildFiber.js +++ b/packages/react-reconciler/src/ReactChildFiber.js @@ -383,8 +383,9 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { // TODO: Split these into branches based on typeof type if ( current !== null && - (current.type === element.type || - (current.tag === Fragment && element.type === REACT_FRAGMENT_TYPE)) + (current.tag === Fragment + ? element.type === REACT_FRAGMENT_TYPE + : current.type === element.type) ) { // Move based on index const existing = useFiber(current, expirationTime); @@ -1240,8 +1241,9 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { if (child.key === key) { // TODO: Split these into branches based on typeof type if ( - child.type === element.type || - (child.tag === Fragment && element.type === REACT_FRAGMENT_TYPE) + child.tag === Fragment + ? element.type === REACT_FRAGMENT_TYPE + : child.type === element.type ) { deleteRemainingChildren(returnFiber, child.sibling); const existing = useFiber(child, expirationTime); From d29bd31102588f59141ea1858c7b97715cfd2dcb Mon Sep 17 00:00:00 2001 From: Clement Hoang Date: Wed, 25 Oct 2017 10:58:05 -0700 Subject: [PATCH 31/43] 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 --- .../src/__tests__/ReactFragment-test.js | 291 +++++++++--------- 1 file changed, 153 insertions(+), 138 deletions(-) diff --git a/packages/react-reconciler/src/__tests__/ReactFragment-test.js b/packages/react-reconciler/src/__tests__/ReactFragment-test.js index ef63187392b63..77a9bbd18c27d 100644 --- a/packages/react-reconciler/src/__tests__/ReactFragment-test.js +++ b/packages/react-reconciler/src/__tests__/ReactFragment-test.js @@ -78,35 +78,21 @@ describe('ReactFragment', () => { } } - function Fragment({condition}) { - return condition - ? - : - -
World
-
; - } - - ReactNoop.render( -
- -
-
, - ); + ReactNoop.render(); ReactNoop.flush(); ReactNoop.render( -
- -
-
, + + +
World
+
, ); ReactNoop.flush(); expect(ops).toEqual(['Update Stateful']); }); - it('should not preserve state of children if no siblings and nested', function() { + it('should not preserve state in non-top-level fragment nesting', function() { var ops = []; class Stateful extends React.Component { @@ -119,25 +105,20 @@ describe('ReactFragment', () => { } } - function Fragment({condition}) { - return condition - ? - : - - - - ; - } - ReactNoop.render(); + ReactNoop.render( + + + , + ); ReactNoop.flush(); - ReactNoop.render(); + ReactNoop.render(); ReactNoop.flush(); expect(ops).toEqual([]); }); - it('should not preserve state of children if nested with siblings', function() { + it('should not preserve state of children if nested 2 levels without siblings', function() { var ops = []; class Stateful extends React.Component { @@ -150,26 +131,22 @@ describe('ReactFragment', () => { } } - function Fragment({condition}) { - return condition - ? - : - - - -
- ; - } - ReactNoop.render(); + ReactNoop.render(); ReactNoop.flush(); - ReactNoop.render(); + ReactNoop.render( + + + + + , + ); ReactNoop.flush(); expect(ops).toEqual([]); }); - it('should preserve state of children in a reorder', function() { + it('should not preserve state of children if nested 2 levels with siblings', function() { var ops = []; class Stateful extends React.Component { @@ -182,33 +159,23 @@ describe('ReactFragment', () => { } } - function Fragment({condition}) { - return condition - ? -
- -
World
- -
- - : - - -
World
-
-
- ; - } - ReactNoop.render(); + ReactNoop.render(); ReactNoop.flush(); - ReactNoop.render(); + ReactNoop.render( + + + + +
+ , + ); ReactNoop.flush(); - expect(ops).toEqual(['Update Stateful']); + expect(ops).toEqual([]); }); - it('should preserve state of children when the keys are same', function() { + it('should not preserve state between array nested in fragment and fragment', function() { var ops = []; class Stateful extends React.Component { @@ -221,28 +188,50 @@ describe('ReactFragment', () => { } } - function Fragment({condition}) { - return condition - ? - - - : - -
- World - ; + ReactNoop.render( + + + , + ); + ReactNoop.flush(); + + ReactNoop.render( + + {[]} + , + ); + ReactNoop.flush(); + + expect(ops).toEqual([]); + }); + + 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
; + } } - ReactNoop.render(); + ReactNoop.render([]); ReactNoop.flush(); - ReactNoop.render(); + ReactNoop.render( + + + , + ); ReactNoop.flush(); expect(ops).toEqual(['Update Stateful']); }); - it('should not preserve state of children when the keys are different', function() { + it('should preserve state between array nested in fragment and double nested fragment', function() { var ops = []; class Stateful extends React.Component { @@ -255,27 +244,42 @@ describe('ReactFragment', () => { } } - function Fragment({condition}) { - return condition - ? - - - : - - World - ; + ReactNoop.render({[]}); + ReactNoop.flush(); + + ReactNoop.render( + + + , + ); + ReactNoop.flush(); + + expect(ops).toEqual(['Update Stateful']); + }); + + it('should 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
; + } } - ReactNoop.render(); + ReactNoop.render({[]}); ReactNoop.flush(); - ReactNoop.render(); + ReactNoop.render([[]]); ReactNoop.flush(); - expect(ops).toEqual([]); + expect(ops).toEqual(['Update Stateful']); }); - it('should preserve state with reordering in multiple levels', function() { + it('should preserve state between double nested fragment and double nested array', function() { var ops = []; class Stateful extends React.Component { @@ -288,35 +292,48 @@ describe('ReactFragment', () => { } } - function Fragment({condition}) { - return condition - ?
- - foo -
- -
-
- boop -
- :
- beep - -
- -
- bar -
-
; + ReactNoop.render( + + + , + ); + ReactNoop.flush(); + + ReactNoop.render([[]]); + ReactNoop.flush(); + + expect(ops).toEqual(['Update Stateful']); + }); + + 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
; + } } - ReactNoop.render(); + ReactNoop.render( + + + , + ); ReactNoop.flush(); - ReactNoop.render(); + ReactNoop.render( + + + World + , + ); ReactNoop.flush(); - expect(ops).toEqual(['Update Stateful']); + expect(ops).toEqual([]); }); it('should preserve state with reordering in multiple levels', function() { @@ -332,32 +349,30 @@ describe('ReactFragment', () => { } } - function Fragment({condition}) { - return condition - ?
- - foo -
- -
-
- boop + ReactNoop.render( +
+ + foo +
+
- :
- beep - -
- -
- bar -
-
; - } - - ReactNoop.render(); +
+ boop +
, + ); ReactNoop.flush(); - ReactNoop.render(); + ReactNoop.render( +
+ beep + +
+ +
+ bar +
+
, + ); ReactNoop.flush(); expect(ops).toEqual(['Update Stateful']); From 8c070e67545b17efa70af0779d4e9d34066b8337 Mon Sep 17 00:00:00 2001 From: Clement Hoang Date: Wed, 25 Oct 2017 19:14:41 -0700 Subject: [PATCH 32/43] Check output of renderer in fragment tests to ensure no silly business despite states being preserved --- .../src/__tests__/ReactFragment-test.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/react-reconciler/src/__tests__/ReactFragment-test.js b/packages/react-reconciler/src/__tests__/ReactFragment-test.js index 77a9bbd18c27d..5206febcf6ec5 100644 --- a/packages/react-reconciler/src/__tests__/ReactFragment-test.js +++ b/packages/react-reconciler/src/__tests__/ReactFragment-test.js @@ -26,6 +26,11 @@ describe('ReactFragment', () => { 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 = ( @@ -90,6 +95,7 @@ describe('ReactFragment', () => { ReactNoop.flush(); expect(ops).toEqual(['Update Stateful']); + expect(ReactNoop.getChildren()).toEqual([div(), div()]); }); it('should not preserve state in non-top-level fragment nesting', function() { @@ -116,6 +122,7 @@ describe('ReactFragment', () => { ReactNoop.flush(); expect(ops).toEqual([]); + expect(ReactNoop.getChildren()).toEqual([div()]); }); it('should not preserve state of children if nested 2 levels without siblings', function() { @@ -144,6 +151,7 @@ describe('ReactFragment', () => { ReactNoop.flush(); expect(ops).toEqual([]); + expect(ReactNoop.getChildren()).toEqual([div()]); }); it('should not preserve state of children if nested 2 levels with siblings', function() { @@ -173,6 +181,7 @@ describe('ReactFragment', () => { ReactNoop.flush(); expect(ops).toEqual([]); + expect(ReactNoop.getChildren()).toEqual([div(), div()]); }); it('should not preserve state between array nested in fragment and fragment', function() { @@ -203,6 +212,7 @@ describe('ReactFragment', () => { ReactNoop.flush(); expect(ops).toEqual([]); + expect(ReactNoop.getChildren()).toEqual([div(), div()]); }); it('should preserve state between top level fragment and array', function() { @@ -229,6 +239,7 @@ describe('ReactFragment', () => { ReactNoop.flush(); expect(ops).toEqual(['Update Stateful']); + expect(ReactNoop.getChildren()).toEqual([div()]); }); it('should preserve state between array nested in fragment and double nested fragment', function() { @@ -255,6 +266,7 @@ describe('ReactFragment', () => { ReactNoop.flush(); expect(ops).toEqual(['Update Stateful']); + expect(ReactNoop.getChildren()).toEqual([div()]); }); it('should preserve state between array nested in fragment and double nested array', function() { @@ -277,6 +289,7 @@ describe('ReactFragment', () => { ReactNoop.flush(); expect(ops).toEqual(['Update Stateful']); + expect(ReactNoop.getChildren()).toEqual([div()]); }); it('should preserve state between double nested fragment and double nested array', function() { @@ -303,6 +316,7 @@ describe('ReactFragment', () => { ReactNoop.flush(); expect(ops).toEqual(['Update Stateful']); + expect(ReactNoop.getChildren()).toEqual([div()]); }); it('should not preserve state of children when the keys are different', function() { @@ -334,6 +348,7 @@ describe('ReactFragment', () => { ReactNoop.flush(); expect(ops).toEqual([]); + expect(ReactNoop.getChildren()).toEqual([div(), span()]); }); it('should preserve state with reordering in multiple levels', function() { @@ -376,5 +391,6 @@ describe('ReactFragment', () => { ReactNoop.flush(); expect(ops).toEqual(['Update Stateful']); + expect(ReactNoop.getChildren()).toEqual([div(span(), div(div()), span())]); }); }); From 545d0b98caf2852641da07ab1687a8f23903ca16 Mon Sep 17 00:00:00 2001 From: Clement Hoang Date: Fri, 27 Oct 2017 02:53:33 -0700 Subject: [PATCH 33/43] Finish implementation of fragment reconciliation with desired behavior --- .../react-reconciler/src/ReactChildFiber.js | 246 +++++------------- packages/react-reconciler/src/ReactFiber.js | 13 - .../src/__tests__/ReactFragment-test.js | 54 +++- 3 files changed, 118 insertions(+), 195 deletions(-) diff --git a/packages/react-reconciler/src/ReactChildFiber.js b/packages/react-reconciler/src/ReactChildFiber.js index 109730b7d8eb7..7b2bcf3a7bc93 100644 --- a/packages/react-reconciler/src/ReactChildFiber.js +++ b/packages/react-reconciler/src/ReactChildFiber.js @@ -381,18 +381,11 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { expirationTime: ExpirationTime, ): Fiber { // TODO: Split these into branches based on typeof type - if ( - current !== null && - (current.tag === Fragment - ? element.type === REACT_FRAGMENT_TYPE - : current.type === element.type) - ) { + if (current !== null && current.type === element.type) { // Move based on index const existing = useFiber(current, expirationTime); existing.ref = coerceRef(current, element); - existing.pendingProps = element.type === REACT_FRAGMENT_TYPE - ? element.props.children - : element.props; + existing.pendingProps = element.props; existing.return = returnFiber; if (__DEV__) { existing._debugSource = element._source; @@ -401,14 +394,25 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { return existing; } else { // Insert - const created = createFiberFromElement( - element, - returnFiber.internalContextTag, - expirationTime, - ); - created.ref = coerceRef(current, 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(current, element); + created.return = returnFiber; + return created; + } } } @@ -538,14 +542,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_COROUTINE_TYPE: { @@ -1267,14 +1282,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 reconcileSingleCoroutine( @@ -1385,140 +1411,9 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { return created; } - // A fork of reconcileChildFibers to be used when reconcileChildFibers - // encounters a top level fragment to treat it as a set of children. - // This function is not recursive. - function reconcileChildFibersForTopLevelFragment( - returnFiber: Fiber, - currentFirstChild: Fiber | null, - newChild: any, - expirationTime: ExpirationTime, - ): Fiber | null { - // This function is not recursive. - // If the top level item is an array, we treat it as a set of children, - // not as a fragment. Nested arrays on the other hand will be treated as - // fragment nodes. Recursion happens at the normal flow. - - // Handle object types - const isObject = typeof newChild === 'object' && newChild !== null; - if (isObject) { - switch (newChild.$$typeof) { - case REACT_ELEMENT_TYPE: - return placeSingleChild( - reconcileSingleElement( - returnFiber, - currentFirstChild, - newChild, - expirationTime, - ), - ); - case REACT_COROUTINE_TYPE: - return placeSingleChild( - reconcileSingleCoroutine( - returnFiber, - currentFirstChild, - newChild, - expirationTime, - ), - ); - case REACT_YIELD_TYPE: - return placeSingleChild( - reconcileSingleYield( - returnFiber, - currentFirstChild, - newChild, - expirationTime, - ), - ); - case REACT_PORTAL_TYPE: - return placeSingleChild( - reconcileSinglePortal( - returnFiber, - currentFirstChild, - newChild, - expirationTime, - ), - ); - } - } - - if (typeof newChild === 'string' || typeof newChild === 'number') { - return placeSingleChild( - reconcileSingleTextNode( - returnFiber, - currentFirstChild, - '' + newChild, - expirationTime, - ), - ); - } - - if (isArray(newChild)) { - return reconcileChildrenArray( - returnFiber, - currentFirstChild, - newChild, - expirationTime, - ); - } - - if (getIteratorFn(newChild)) { - return reconcileChildrenIterator( - returnFiber, - currentFirstChild, - newChild, - expirationTime, - ); - } - - if (isObject) { - throwOnInvalidObjectType(returnFiber, newChild); - } - - if (__DEV__) { - if (typeof newChild === 'function') { - warnOnFunctionType(); - } - } - if (typeof newChild === 'undefined') { - // If the new child is undefined, and the return fiber is a composite - // component, throw an error. If Fiber return types are disabled, - // we already threw above. - switch (returnFiber.tag) { - case ClassComponent: { - if (__DEV__) { - const instance = returnFiber.stateNode; - if (instance.render._isMockFunction) { - // We allow auto-mocks to proceed as if they're returning null. - break; - } - } - } - // Intentionally fall through to the next case, which handles both - // functions and classes - // eslint-disable-next-lined no-fallthrough - case FunctionalComponent: { - const Component = returnFiber.type; - invariant( - false, - '%s(...): Nothing was returned from render. This usually means a ' + - 'return statement is missing. Or, to render nothing, ' + - 'return null.', - Component.displayName || Component.name || 'Component', - ); - } - } - } - - // Remaining cases are all treated as empty. - return deleteRemainingChildren(returnFiber, currentFirstChild); - } - // This API will tag the children with the side-effect of the reconciliation // itself. They will be added to the side-effect list as we pass through the // children and the parent. - // This function is forked at reconcileChildFibersForTopLevelFragment - // Please reflect changes in this function to its fork as well function reconcileChildFibers( returnFiber: Fiber, currentFirstChild: Fiber | null, @@ -1532,20 +1427,21 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { // Handle object types const isObject = typeof newChild === 'object' && newChild !== null; + + // 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 ( + isObject && + newChild.type === REACT_FRAGMENT_TYPE && + newChild.key === null + ) { + newChild = newChild.props.children; + } + if (isObject) { switch (newChild.$$typeof) { case REACT_ELEMENT_TYPE: - // This function recurses only on a top-level fragment, - // so that it is treated as a set of children - // Otherwise, we follow the normal flow. - if (newChild.type === REACT_FRAGMENT_TYPE && newChild.key === null) { - return reconcileChildFibersForTopLevelFragment( - returnFiber, - currentFirstChild, - newChild.props.children, - expirationTime, - ); - } return placeSingleChild( reconcileSingleElement( returnFiber, diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index 8f68ed9f77551..e868f29fe5fdb 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -43,12 +43,6 @@ var {NoEffect} = require('ReactTypeOfSideEffect'); var invariant = require('fbjs/lib/invariant'); -const REACT_FRAGMENT_TYPE = - (typeof Symbol === 'function' && - Symbol.for && - Symbol.for('react.fragment')) || - 0xeacb; - if (__DEV__) { var getComponentName = require('getComponentName'); var hasBadMapPolyfill = false; @@ -322,13 +316,6 @@ exports.createFiberFromElement = function( fiber = createFiber(HostComponent, key, internalContextTag); fiber.type = type; fiber.pendingProps = element.props; - } else if (type === REACT_FRAGMENT_TYPE) { - fiber = createFiberFromFragment( - element.props.children, - internalContextTag, - expirationTime, - key, - ); } else if ( typeof type === 'object' && type !== null && diff --git a/packages/react-reconciler/src/__tests__/ReactFragment-test.js b/packages/react-reconciler/src/__tests__/ReactFragment-test.js index 5206febcf6ec5..e817c01b34f80 100644 --- a/packages/react-reconciler/src/__tests__/ReactFragment-test.js +++ b/packages/react-reconciler/src/__tests__/ReactFragment-test.js @@ -98,6 +98,46 @@ describe('ReactFragment', () => { expect(ReactNoop.getChildren()).toEqual([div(), 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
; + } + } + + ReactNoop.render( + + + + + + + , + ); + ReactNoop.flush(); + + ReactNoop.render( + + + +
+ + + + , + ); + ReactNoop.flush(); + + expect(ops).toEqual(['Update Stateful']); + expect(ReactNoop.getChildren()).toEqual([div(), div()]); + }); + it('should not preserve state in non-top-level fragment nesting', function() { var ops = []; @@ -184,7 +224,7 @@ describe('ReactFragment', () => { expect(ReactNoop.getChildren()).toEqual([div(), div()]); }); - it('should not preserve state between array nested in fragment and fragment', function() { + it('should preserve state between array nested in fragment and fragment', function() { var ops = []; class Stateful extends React.Component { @@ -211,8 +251,8 @@ describe('ReactFragment', () => { ); ReactNoop.flush(); - expect(ops).toEqual([]); - expect(ReactNoop.getChildren()).toEqual([div(), div()]); + expect(ops).toEqual(['Update Stateful']); + expect(ReactNoop.getChildren()).toEqual([div()]); }); it('should preserve state between top level fragment and array', function() { @@ -242,7 +282,7 @@ describe('ReactFragment', () => { expect(ReactNoop.getChildren()).toEqual([div()]); }); - it('should preserve state between array nested in fragment and double nested fragment', function() { + it('should not preserve state between array nested in fragment and double nested fragment', function() { var ops = []; class Stateful extends React.Component { @@ -265,11 +305,11 @@ describe('ReactFragment', () => { ); ReactNoop.flush(); - expect(ops).toEqual(['Update Stateful']); + expect(ops).toEqual([]); expect(ReactNoop.getChildren()).toEqual([div()]); }); - it('should preserve state between array nested in fragment and double nested array', function() { + it('should not preserve state between array nested in fragment and double nested array', function() { var ops = []; class Stateful extends React.Component { @@ -288,7 +328,7 @@ describe('ReactFragment', () => { ReactNoop.render([[]]); ReactNoop.flush(); - expect(ops).toEqual(['Update Stateful']); + expect(ops).toEqual([]); expect(ReactNoop.getChildren()).toEqual([div()]); }); From c8a075280b506880f80f06feb5f45a84e61826bf Mon Sep 17 00:00:00 2001 From: Clement Hoang Date: Fri, 27 Oct 2017 03:23:16 -0700 Subject: [PATCH 34/43] Add reverse render direction for fragment tests --- .../src/__tests__/ReactFragment-test.js | 334 ++++++++++++------ 1 file changed, 224 insertions(+), 110 deletions(-) diff --git a/packages/react-reconciler/src/__tests__/ReactFragment-test.js b/packages/react-reconciler/src/__tests__/ReactFragment-test.js index e817c01b34f80..cb17ae3a1cd73 100644 --- a/packages/react-reconciler/src/__tests__/ReactFragment-test.js +++ b/packages/react-reconciler/src/__tests__/ReactFragment-test.js @@ -83,19 +83,29 @@ describe('ReactFragment', () => { } } - ReactNoop.render(); + function Foo({condition}) { + return condition + ? + : + +
World
+
; + } + + ReactNoop.render(); ReactNoop.flush(); - ReactNoop.render( - - -
World
-
, - ); + 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 of children nested at same level', function() { @@ -111,31 +121,39 @@ describe('ReactFragment', () => { } } - ReactNoop.render( - - - - + function Foo({condition}) { + return condition + ? + + + + + - - , - ); + : + + +
+ + + + ; + } + + ReactNoop.render(); ReactNoop.flush(); - ReactNoop.render( - - - -
- - - - , - ); + 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() { @@ -151,14 +169,24 @@ describe('ReactFragment', () => { } } - ReactNoop.render( - - - , - ); + function Foo({condition}) { + return condition + ? + + + : ; + } + + ReactNoop.render(); + ReactNoop.flush(); + + ReactNoop.render(); ReactNoop.flush(); - ReactNoop.render(); + expect(ops).toEqual([]); + expect(ReactNoop.getChildren()).toEqual([div()]); + + ReactNoop.render(); ReactNoop.flush(); expect(ops).toEqual([]); @@ -178,16 +206,26 @@ describe('ReactFragment', () => { } } - ReactNoop.render(); + function Foo({condition}) { + return condition + ? + : + + + + ; + } + + ReactNoop.render(); ReactNoop.flush(); - ReactNoop.render( - - - - - , - ); + ReactNoop.render(); + ReactNoop.flush(); + + expect(ops).toEqual([]); + expect(ReactNoop.getChildren()).toEqual([div()]); + + ReactNoop.render(); ReactNoop.flush(); expect(ops).toEqual([]); @@ -207,21 +245,31 @@ describe('ReactFragment', () => { } } - ReactNoop.render(); + function Foo({condition}) { + return condition + ? + : + + + +
+ ; + } + + ReactNoop.render(); ReactNoop.flush(); - ReactNoop.render( - - - - -
- , - ); + 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() { @@ -237,22 +285,30 @@ describe('ReactFragment', () => { } } - ReactNoop.render( - - - , - ); + function Foo({condition}) { + return condition + ? + + + : + {[]} + ; + } + + ReactNoop.render(); ReactNoop.flush(); - ReactNoop.render( - - {[]} - , - ); + 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() { @@ -268,18 +324,28 @@ describe('ReactFragment', () => { } } - ReactNoop.render([]); + function Foo({condition}) { + return condition + ? [] + : + + ; + } + + ReactNoop.render(); ReactNoop.flush(); - ReactNoop.render( - - - , - ); + 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() { @@ -295,14 +361,24 @@ describe('ReactFragment', () => { } } - ReactNoop.render({[]}); + function Foo({condition}) { + return condition + ? {[]} + : + + ; + } + + ReactNoop.render(); ReactNoop.flush(); - ReactNoop.render( - - - , - ); + ReactNoop.render(); + ReactNoop.flush(); + + expect(ops).toEqual([]); + expect(ReactNoop.getChildren()).toEqual([div()]); + + ReactNoop.render(); ReactNoop.flush(); expect(ops).toEqual([]); @@ -322,10 +398,22 @@ describe('ReactFragment', () => { } } - ReactNoop.render({[]}); + function Foo({condition}) { + return condition + ? {[]} + : [[]]; + } + + ReactNoop.render(); ReactNoop.flush(); - ReactNoop.render([[]]); + ReactNoop.render(); + ReactNoop.flush(); + + expect(ops).toEqual([]); + expect(ReactNoop.getChildren()).toEqual([div()]); + + ReactNoop.render(); ReactNoop.flush(); expect(ops).toEqual([]); @@ -345,18 +433,28 @@ describe('ReactFragment', () => { } } - ReactNoop.render( - - - , - ); + function Foo({condition}) { + return condition + ? + + + : [[]]; + } + + ReactNoop.render(); ReactNoop.flush(); - ReactNoop.render([[]]); + 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() { @@ -372,23 +470,31 @@ describe('ReactFragment', () => { } } - ReactNoop.render( - - - , - ); + function Foo({condition}) { + return condition + ? + + + : + + World + ; + } + + ReactNoop.render(); ReactNoop.flush(); - ReactNoop.render( - - - World - , - ); + 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 preserve state with reordering in multiple levels', function() { @@ -404,33 +510,41 @@ describe('ReactFragment', () => { } } - ReactNoop.render( -
- - foo -
- + function Foo({condition}) { + return condition + ?
+ + foo +
+ +
+
+ boop
- - boop -
, - ); + :
+ beep + +
+ +
+ bar +
+
; + } + + ReactNoop.render(); ReactNoop.flush(); - ReactNoop.render( -
- beep - -
- -
- bar -
-
, - ); + 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())]); }); }); From 48bcaa7198ef5d97bfc78badd1636a836a076cda Mon Sep 17 00:00:00 2001 From: Clement Hoang Date: Fri, 27 Oct 2017 04:09:40 -0700 Subject: [PATCH 35/43] Remove unneeded fragment branch in updateElement --- .../react-reconciler/src/ReactChildFiber.js | 27 ++++++------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/packages/react-reconciler/src/ReactChildFiber.js b/packages/react-reconciler/src/ReactChildFiber.js index 70b29efaf224d..781663e7df1df 100644 --- a/packages/react-reconciler/src/ReactChildFiber.js +++ b/packages/react-reconciler/src/ReactChildFiber.js @@ -402,25 +402,14 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { return existing; } else { // Insert - 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(current, element); - created.return = returnFiber; - return created; - } + const created = createFiberFromElement( + element, + returnFiber.internalContextTag, + expirationTime, + ); + created.ref = coerceRef(current, element); + created.return = returnFiber; + return created; } } From b3864af0f12f94b741fa0e4e5b43cedc478751a3 Mon Sep 17 00:00:00 2001 From: Clement Hoang Date: Fri, 27 Oct 2017 11:11:03 -0700 Subject: [PATCH 36/43] Add more test cases for ReactFragment --- .../src/__tests__/ReactFragment-test.js | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/packages/react-reconciler/src/__tests__/ReactFragment-test.js b/packages/react-reconciler/src/__tests__/ReactFragment-test.js index cb17ae3a1cd73..a1d7c8cbc7735 100644 --- a/packages/react-reconciler/src/__tests__/ReactFragment-test.js +++ b/packages/react-reconciler/src/__tests__/ReactFragment-test.js @@ -108,6 +108,45 @@ describe('ReactFragment', () => { 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 = []; @@ -497,6 +536,45 @@ describe('ReactFragment', () => { 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 = []; From 17ac4a219a3bc6cc211ae18cf264d197f225dbbf Mon Sep 17 00:00:00 2001 From: Clement Hoang Date: Fri, 27 Oct 2017 14:56:15 -0700 Subject: [PATCH 37/43] Handle childless fragment in reconciler --- packages/react-reconciler/src/ReactChildFiber.js | 9 +++++---- .../react-reconciler/src/__tests__/ReactFragment-test.js | 9 +++++++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/react-reconciler/src/ReactChildFiber.js b/packages/react-reconciler/src/ReactChildFiber.js index 781663e7df1df..e700355b21671 100644 --- a/packages/react-reconciler/src/ReactChildFiber.js +++ b/packages/react-reconciler/src/ReactChildFiber.js @@ -1422,20 +1422,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 object types - const isObject = typeof newChild === 'object' && newChild !== null; - // 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 ( - isObject && + 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: diff --git a/packages/react-reconciler/src/__tests__/ReactFragment-test.js b/packages/react-reconciler/src/__tests__/ReactFragment-test.js index a1d7c8cbc7735..3faf9c5b62def 100644 --- a/packages/react-reconciler/src/__tests__/ReactFragment-test.js +++ b/packages/react-reconciler/src/__tests__/ReactFragment-test.js @@ -44,6 +44,15 @@ describe('ReactFragment', () => { 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 = ( From 27312d0ebbc3ebbdaa8dc48e6f70119847267389 Mon Sep 17 00:00:00 2001 From: Clement Hoang Date: Fri, 27 Oct 2017 15:26:40 -0700 Subject: [PATCH 38/43] Support fragment flattening in SSR --- .../ReactDOMServerIntegration-test.js | 50 +++++++++++++++++++ .../src/server/ReactPartialRenderer.js | 44 +++++++++++++--- 2 files changed, 87 insertions(+), 7 deletions(-) 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 +
+