diff --git a/packages/react-call-return/README.md b/packages/react-call-return/README.md new file mode 100644 index 0000000000000..3c774bc2c0ca3 --- /dev/null +++ b/packages/react-call-return/README.md @@ -0,0 +1,11 @@ +# react-call-return + +This is an experimental package for multi-pass rendering in React. + +**Its API is not as stable as that of React, React Native, or React DOM, and does not follow the common versioning scheme.** + +**Use it at your own risk.** + +# API + +See the test case in `src/__tests__/ReactCallReturn.js` for an example. diff --git a/packages/react-call-return/index.js b/packages/react-call-return/index.js new file mode 100644 index 0000000000000..8d3798337c698 --- /dev/null +++ b/packages/react-call-return/index.js @@ -0,0 +1,12 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +'use strict'; + +module.exports = require('./src/ReactCallReturn'); diff --git a/packages/react-call-return/npm/index.js b/packages/react-call-return/npm/index.js new file mode 100644 index 0000000000000..0856ded523d75 --- /dev/null +++ b/packages/react-call-return/npm/index.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-call-return.production.min.js'); +} else { + module.exports = require('./cjs/react-call-return.development.js'); +} diff --git a/packages/react-call-return/package.json b/packages/react-call-return/package.json new file mode 100644 index 0000000000000..2cf21889b88d5 --- /dev/null +++ b/packages/react-call-return/package.json @@ -0,0 +1,13 @@ +{ + "name": "react-call-return", + "description": "Experimental APIs for multi-pass rendering in React.", + "version": "0.1.0", + "repository": "facebook/react", + "dependencies": { + "fbjs": "^0.8.16", + "object-assign": "^4.1.1" + }, + "peerDependencies": { + "react": "^16.0.0" + } +} diff --git a/packages/react-call-return/src/ReactCallReturn.js b/packages/react-call-return/src/ReactCallReturn.js new file mode 100644 index 0000000000000..d3f814d2adb50 --- /dev/null +++ b/packages/react-call-return/src/ReactCallReturn.js @@ -0,0 +1,94 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +'use strict'; + +import type {ReactCall, ReactNodeList, ReactReturn} from 'shared/ReactTypes'; + +// The Symbol used to tag the special React types. If there is no native Symbol +// nor polyfill, then a plain number is used for performance. +var REACT_CALL_TYPE; +var REACT_RETURN_TYPE; +if (typeof Symbol === 'function' && Symbol.for) { + REACT_CALL_TYPE = Symbol.for('react.call'); + REACT_RETURN_TYPE = Symbol.for('react.return'); +} else { + REACT_CALL_TYPE = 0xeac8; + REACT_RETURN_TYPE = 0xeac9; +} + +type CallHandler = (props: T, returns: Array) => ReactNodeList; + +exports.unstable_createCall = function( + children: mixed, + handler: CallHandler, + props: T, + key: ?string = null, +): ReactCall { + var call = { + // This tag allow us to uniquely identify this as a React Call + $$typeof: REACT_CALL_TYPE, + key: key == null ? null : '' + key, + children: children, + handler: handler, + props: props, + }; + + if (__DEV__) { + // TODO: Add _store property for marking this as validated. + if (Object.freeze) { + Object.freeze(call.props); + Object.freeze(call); + } + } + + return call; +}; + +exports.unstable_createReturn = function(value: mixed): ReactReturn { + var returnNode = { + // This tag allow us to uniquely identify this as a React Return + $$typeof: REACT_RETURN_TYPE, + value: value, + }; + + if (__DEV__) { + // TODO: Add _store property for marking this as validated. + if (Object.freeze) { + Object.freeze(returnNode); + } + } + + return returnNode; +}; + +/** + * Verifies the object is a call object. + */ +exports.unstable_isCall = function(object: mixed): boolean { + return ( + typeof object === 'object' && + object !== null && + object.$$typeof === REACT_CALL_TYPE + ); +}; + +/** + * Verifies the object is a return object. + */ +exports.unstable_isReturn = function(object: mixed): boolean { + return ( + typeof object === 'object' && + object !== null && + object.$$typeof === REACT_RETURN_TYPE + ); +}; + +exports.unstable_REACT_RETURN_TYPE = REACT_RETURN_TYPE; +exports.unstable_REACT_CALL_TYPE = REACT_CALL_TYPE; diff --git a/packages/react-reconciler/src/__tests__/ReactCoroutine-test.js b/packages/react-call-return/src/__tests__/ReactCallReturn-test.js similarity index 74% rename from packages/react-reconciler/src/__tests__/ReactCoroutine-test.js rename to packages/react-call-return/src/__tests__/ReactCallReturn-test.js index fde4ded1f0727..1920c33cfa514 100644 --- a/packages/react-reconciler/src/__tests__/ReactCoroutine-test.js +++ b/packages/react-call-return/src/__tests__/ReactCallReturn-test.js @@ -11,16 +11,14 @@ var React; var ReactNoop; -var ReactCoroutine; +var ReactCallReturn; -describe('ReactCoroutine', () => { +describe('ReactCallReturn', () => { beforeEach(() => { jest.resetModules(); React = require('react'); ReactNoop = require('react-noop-renderer'); - // TODO: can we express this test with only public API? - // TODO: direct imports like some-package/src/* are bad. Fix me. - ReactCoroutine = require('react-reconciler/src/ReactCoroutine'); + ReactCallReturn = require('react-call-return'); }); function div(...children) { @@ -32,7 +30,7 @@ describe('ReactCoroutine', () => { return {type: 'span', children: [], prop}; } - it('should render a coroutine', () => { + it('should render a call', () => { var ops = []; function Continuation({isSame}) { @@ -41,10 +39,10 @@ describe('ReactCoroutine', () => { } // An alternative API could mark Continuation as something that needs - // yielding. E.g. Continuation.yieldType = 123; + // returning. E.g. Continuation.returnType = 123; function Child({bar}) { ops.push(['Child', bar]); - return ReactCoroutine.createYield({ + return ReactCallReturn.unstable_createReturn({ props: { bar: bar, }, @@ -57,20 +55,20 @@ describe('ReactCoroutine', () => { return [, ]; } - function HandleYields(props, yields) { - ops.push('HandleYields'); - return yields.map((y, i) => ( + function HandleReturns(props, returns) { + ops.push('HandleReturns'); + return returns.map((y, i) => ( )); } // An alternative API could mark Parent as something that needs - // yielding. E.g. Parent.handler = HandleYields; + // returning. E.g. Parent.handler = HandleReturns; function Parent(props) { ops.push('Parent'); - return ReactCoroutine.createCoroutine( + return ReactCallReturn.unstable_createCall( props.children, - HandleYields, + HandleReturns, props, ); } @@ -86,11 +84,11 @@ describe('ReactCoroutine', () => { 'Parent', 'Indirection', ['Child', true], - // Yield + // Return ['Child', false], - // Yield - 'HandleYields', - // Continue yields + // Return + 'HandleReturns', + // Call continuations ['Continuation', true], ['Continuation', false], ]); @@ -99,13 +97,13 @@ describe('ReactCoroutine', () => { ]); }); - it('should update a coroutine', () => { + it('should update a call', () => { function Continuation({isSame}) { return ; } function Child({bar}) { - return ReactCoroutine.createYield({ + return ReactCallReturn.unstable_createReturn({ props: { bar: bar, }, @@ -117,16 +115,16 @@ describe('ReactCoroutine', () => { return [, ]; } - function HandleYields(props, yields) { - return yields.map((y, i) => ( + function HandleReturns(props, returns) { + return returns.map((y, i) => ( )); } function Parent(props) { - return ReactCoroutine.createCoroutine( + return ReactCallReturn.unstable_createCall( props.children, - HandleYields, + HandleReturns, props, ); } @@ -148,7 +146,7 @@ describe('ReactCoroutine', () => { ]); }); - it('should unmount a composite in a coroutine', () => { + it('should unmount a composite in a call', () => { var ops = []; class Continuation extends React.Component { @@ -164,16 +162,16 @@ describe('ReactCoroutine', () => { class Child extends React.Component { render() { ops.push('Child'); - return ReactCoroutine.createYield(Continuation); + return ReactCallReturn.unstable_createReturn(Continuation); } componentWillUnmount() { ops.push('Unmount Child'); } } - function HandleYields(props, yields) { - ops.push('HandleYields'); - return yields.map((ContinuationComponent, i) => ( + function HandleReturns(props, returns) { + ops.push('HandleReturns'); + return returns.map((ContinuationComponent, i) => ( )); } @@ -181,9 +179,9 @@ describe('ReactCoroutine', () => { class Parent extends React.Component { render() { ops.push('Parent'); - return ReactCoroutine.createCoroutine( + return ReactCallReturn.unstable_createCall( this.props.children, - HandleYields, + HandleReturns, this.props, ); } @@ -195,7 +193,7 @@ describe('ReactCoroutine', () => { ReactNoop.render(); ReactNoop.flush(); - expect(ops).toEqual(['Parent', 'Child', 'HandleYields', 'Continuation']); + expect(ops).toEqual(['Parent', 'Child', 'HandleReturns', 'Continuation']); ops = []; @@ -209,25 +207,25 @@ describe('ReactCoroutine', () => { ]); }); - it('should handle deep updates in coroutine', () => { + it('should handle deep updates in call', () => { let instances = {}; class Counter extends React.Component { state = {value: 5}; render() { instances[this.props.id] = this; - return ReactCoroutine.createYield(this.state.value); + return ReactCallReturn.unstable_createReturn(this.state.value); } } function App(props) { - return ReactCoroutine.createCoroutine( + return ReactCallReturn.unstable_createCall( [ , , , ], - (p, yields) => yields.map((y, i) => ), + (p, returns) => returns.map((y, i) => ), {}, ); } diff --git a/packages/react-reconciler/src/ReactChildFiber.js b/packages/react-reconciler/src/ReactChildFiber.js index 9b95e0d28f74f..994ef74e6a278 100644 --- a/packages/react-reconciler/src/ReactChildFiber.js +++ b/packages/react-reconciler/src/ReactChildFiber.js @@ -10,7 +10,7 @@ 'use strict'; import type {ReactElement} from 'shared/ReactElementType'; -import type {ReactCoroutine, ReactPortal, ReactYield} from 'shared/ReactTypes'; +import type {ReactCall, ReactPortal, ReactReturn} from 'shared/ReactTypes'; import type {Fiber} from 'react-reconciler/src/ReactFiber'; import type { ExpirationTime, @@ -20,8 +20,6 @@ var ReactTypeOfSideEffect = require('shared/ReactTypeOfSideEffect'); var ReactTypeOfWork = require('shared/ReactTypeOfWork'); var emptyObject = require('fbjs/lib/emptyObject'); var invariant = require('fbjs/lib/invariant'); - -var {REACT_COROUTINE_TYPE, REACT_YIELD_TYPE} = require('./ReactCoroutine'); var {REACT_PORTAL_TYPE} = require('./ReactPortal'); var ReactFiber = require('./ReactFiber'); @@ -78,8 +76,8 @@ const { createFiberFromElement, createFiberFromFragment, createFiberFromText, - createFiberFromCoroutine, - createFiberFromYield, + createFiberFromCall, + createFiberFromReturn, createFiberFromPortal, } = ReactFiber; @@ -90,8 +88,8 @@ const { ClassComponent, HostText, HostPortal, - CoroutineComponent, - YieldComponent, + CallComponent, + ReturnComponent, Fragment, } = ReactTypeOfWork; @@ -99,11 +97,21 @@ const {NoEffect, Placement, Deletion} = ReactTypeOfSideEffect; const ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator; const FAUX_ITERATOR_SYMBOL = '@@iterator'; // Before Symbol spec. -// The Symbol used to tag the ReactElement type. If there is no native Symbol + +// The Symbol used to tag the ReactElement-like types. If there is no native Symbol // nor polyfill, then a plain number is used for performance. -const REACT_ELEMENT_TYPE = - (typeof Symbol === 'function' && Symbol.for && Symbol.for('react.element')) || - 0xeac7; +var REACT_ELEMENT_TYPE; +var REACT_CALL_TYPE; +var REACT_RETURN_TYPE; +if (typeof Symbol === 'function' && Symbol.for) { + REACT_ELEMENT_TYPE = Symbol.for('react.element'); + REACT_CALL_TYPE = Symbol.for('react.call'); + REACT_RETURN_TYPE = Symbol.for('react.return'); +} else { + REACT_ELEMENT_TYPE = 0xeac7; + REACT_CALL_TYPE = 0xeac8; + REACT_RETURN_TYPE = 0xeac9; +} function getIteratorFn(maybeIterable: ?any): ?() => ?Iterator<*> { if (maybeIterable === null || typeof maybeIterable === 'undefined') { @@ -401,17 +409,17 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { } } - function updateCoroutine( + function updateCall( returnFiber: Fiber, current: Fiber | null, - coroutine: ReactCoroutine, + call: ReactCall, expirationTime: ExpirationTime, ): Fiber { // TODO: Should this also compare handler to determine whether to reuse? - if (current === null || current.tag !== CoroutineComponent) { + if (current === null || current.tag !== CallComponent) { // Insert - const created = createFiberFromCoroutine( - coroutine, + const created = createFiberFromCall( + call, returnFiber.internalContextTag, expirationTime, ); @@ -420,32 +428,32 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { } else { // Move based on index const existing = useFiber(current, expirationTime); - existing.pendingProps = coroutine; + existing.pendingProps = call; existing.return = returnFiber; return existing; } } - function updateYield( + function updateReturn( returnFiber: Fiber, current: Fiber | null, - yieldNode: ReactYield, + returnNode: ReactReturn, expirationTime: ExpirationTime, ): Fiber { - if (current === null || current.tag !== YieldComponent) { + if (current === null || current.tag !== ReturnComponent) { // Insert - const created = createFiberFromYield( - yieldNode, + const created = createFiberFromReturn( + returnNode, returnFiber.internalContextTag, expirationTime, ); - created.type = yieldNode.value; + created.type = returnNode.value; created.return = returnFiber; return created; } else { // Move based on index const existing = useFiber(current, expirationTime); - existing.type = yieldNode.value; + existing.type = returnNode.value; existing.return = returnFiber; return existing; } @@ -535,8 +543,8 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { return created; } - case REACT_COROUTINE_TYPE: { - const created = createFiberFromCoroutine( + case REACT_CALL_TYPE: { + const created = createFiberFromCall( newChild, returnFiber.internalContextTag, expirationTime, @@ -545,8 +553,8 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { return created; } - case REACT_YIELD_TYPE: { - const created = createFiberFromYield( + case REACT_RETURN_TYPE: { + const created = createFiberFromReturn( newChild, returnFiber.internalContextTag, expirationTime, @@ -629,25 +637,25 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { } } - case REACT_COROUTINE_TYPE: { + case REACT_CALL_TYPE: { if (newChild.key === key) { - return updateCoroutine( - returnFiber, - oldFiber, - newChild, - expirationTime, - ); + return updateCall(returnFiber, oldFiber, newChild, expirationTime); } else { return null; } } - case REACT_YIELD_TYPE: { - // Yields don't have keys. If the previous node is implicitly keyed + case REACT_RETURN_TYPE: { + // Returns don't have keys. If the previous node is implicitly keyed // we can continue to replace it without aborting even if it is not a // yield. if (key === null) { - return updateYield(returnFiber, oldFiber, newChild, expirationTime); + return updateReturn( + returnFiber, + oldFiber, + newChild, + expirationTime, + ); } else { return null; } @@ -722,12 +730,12 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { ); } - case REACT_COROUTINE_TYPE: { + case REACT_CALL_TYPE: { const matchedFiber = existingChildren.get( newChild.key === null ? newIdx : newChild.key, ) || null; - return updateCoroutine( + return updateCall( returnFiber, matchedFiber, newChild, @@ -735,11 +743,11 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { ); } - case REACT_YIELD_TYPE: { - // Yields don't have keys, so we neither have to check the old nor - // new node for the key. If both are yields, they match. + case REACT_RETURN_TYPE: { + // Returns don't have keys, so we neither have to check the old nor + // new node for the key. If both are returns, they match. const matchedFiber = existingChildren.get(newIdx) || null; - return updateYield( + return updateReturn( returnFiber, matchedFiber, newChild, @@ -796,7 +804,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { } switch (child.$$typeof) { case REACT_ELEMENT_TYPE: - case REACT_COROUTINE_TYPE: + case REACT_CALL_TYPE: case REACT_PORTAL_TYPE: warnForMissingKey(child); const key = child.key; @@ -1236,22 +1244,22 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { return created; } - function reconcileSingleCoroutine( + function reconcileSingleCall( returnFiber: Fiber, currentFirstChild: Fiber | null, - coroutine: ReactCoroutine, + call: ReactCall, expirationTime: ExpirationTime, ): Fiber { - const key = coroutine.key; + const key = call.key; let child = currentFirstChild; while (child !== null) { // 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.tag === CoroutineComponent) { + if (child.tag === CallComponent) { deleteRemainingChildren(returnFiber, child.sibling); const existing = useFiber(child, expirationTime); - existing.pendingProps = coroutine; + existing.pendingProps = call; existing.return = returnFiber; return existing; } else { @@ -1264,8 +1272,8 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { child = child.sibling; } - const created = createFiberFromCoroutine( - coroutine, + const created = createFiberFromCall( + call, returnFiber.internalContextTag, expirationTime, ); @@ -1273,19 +1281,19 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { return created; } - function reconcileSingleYield( + function reconcileSingleReturn( returnFiber: Fiber, currentFirstChild: Fiber | null, - yieldNode: ReactYield, + returnNode: ReactReturn, expirationTime: ExpirationTime, ): Fiber { // There's no need to check for keys on yields since they're stateless. let child = currentFirstChild; if (child !== null) { - if (child.tag === YieldComponent) { + if (child.tag === ReturnComponent) { deleteRemainingChildren(returnFiber, child.sibling); const existing = useFiber(child, expirationTime); - existing.type = yieldNode.value; + existing.type = returnNode.value; existing.return = returnFiber; return existing; } else { @@ -1293,12 +1301,12 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { } } - const created = createFiberFromYield( - yieldNode, + const created = createFiberFromReturn( + returnNode, returnFiber.internalContextTag, expirationTime, ); - created.type = yieldNode.value; + created.type = returnNode.value; created.return = returnFiber; return created; } @@ -1372,18 +1380,18 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { ), ); - case REACT_COROUTINE_TYPE: + case REACT_CALL_TYPE: return placeSingleChild( - reconcileSingleCoroutine( + reconcileSingleCall( returnFiber, currentFirstChild, newChild, expirationTime, ), ); - case REACT_YIELD_TYPE: + case REACT_RETURN_TYPE: return placeSingleChild( - reconcileSingleYield( + reconcileSingleReturn( returnFiber, currentFirstChild, newChild, diff --git a/packages/react-reconciler/src/ReactCoroutine.js b/packages/react-reconciler/src/ReactCoroutine.js deleted file mode 100644 index 749fe94b421c8..0000000000000 --- a/packages/react-reconciler/src/ReactCoroutine.js +++ /dev/null @@ -1,98 +0,0 @@ -/** - * Copyright (c) 2014-present, Facebook, Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -'use strict'; - -import type { - ReactCoroutine, - ReactNodeList, - ReactYield, -} from 'shared/ReactTypes'; - -// The Symbol used to tag the special React types. If there is no native Symbol -// nor polyfill, then a plain number is used for performance. -var REACT_COROUTINE_TYPE; -var REACT_YIELD_TYPE; -if (typeof Symbol === 'function' && Symbol.for) { - REACT_COROUTINE_TYPE = Symbol.for('react.coroutine'); - REACT_YIELD_TYPE = Symbol.for('react.yield'); -} else { - REACT_COROUTINE_TYPE = 0xeac8; - REACT_YIELD_TYPE = 0xeac9; -} - -type CoroutineHandler = (props: T, yields: Array) => ReactNodeList; - -exports.createCoroutine = function( - children: mixed, - handler: CoroutineHandler, - props: T, - key: ?string = null, -): ReactCoroutine { - var coroutine = { - // This tag allow us to uniquely identify this as a React Coroutine - $$typeof: REACT_COROUTINE_TYPE, - key: key == null ? null : '' + key, - children: children, - handler: handler, - props: props, - }; - - if (__DEV__) { - // TODO: Add _store property for marking this as validated. - if (Object.freeze) { - Object.freeze(coroutine.props); - Object.freeze(coroutine); - } - } - - return coroutine; -}; - -exports.createYield = function(value: mixed): ReactYield { - var yieldNode = { - // This tag allow us to uniquely identify this as a React Yield - $$typeof: REACT_YIELD_TYPE, - value: value, - }; - - if (__DEV__) { - // TODO: Add _store property for marking this as validated. - if (Object.freeze) { - Object.freeze(yieldNode); - } - } - - return yieldNode; -}; - -/** - * Verifies the object is a coroutine object. - */ -exports.isCoroutine = function(object: mixed): boolean { - return ( - typeof object === 'object' && - object !== null && - object.$$typeof === REACT_COROUTINE_TYPE - ); -}; - -/** - * Verifies the object is a yield object. - */ -exports.isYield = function(object: mixed): boolean { - return ( - typeof object === 'object' && - object !== null && - object.$$typeof === REACT_YIELD_TYPE - ); -}; - -exports.REACT_YIELD_TYPE = REACT_YIELD_TYPE; -exports.REACT_COROUTINE_TYPE = REACT_COROUTINE_TYPE; diff --git a/packages/react-reconciler/src/ReactDebugFiberPerf.js b/packages/react-reconciler/src/ReactDebugFiberPerf.js index 39a0d75ff89ac..744cfc5390a0b 100644 --- a/packages/react-reconciler/src/ReactDebugFiberPerf.js +++ b/packages/react-reconciler/src/ReactDebugFiberPerf.js @@ -29,7 +29,7 @@ if (__DEV__) { HostComponent, HostText, HostPortal, - YieldComponent, + ReturnComponent, Fragment, } = require('shared/ReactTypeOfWork'); @@ -168,7 +168,7 @@ if (__DEV__) { case HostComponent: case HostText: case HostPortal: - case YieldComponent: + case ReturnComponent: case Fragment: return true; default: diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index 6a92e87c17622..1ee7867efdd07 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -11,10 +11,10 @@ import type {ReactElement, Source} from 'shared/ReactElementType'; import type { - ReactCoroutine, + ReactCall, ReactFragment, ReactPortal, - ReactYield, + ReactReturn, } from 'shared/ReactTypes'; import type {TypeOfWork} from 'shared/ReactTypeOfWork'; import type {TypeOfInternalContext} from './ReactTypeOfInternalContext'; @@ -31,8 +31,8 @@ var { HostComponent, HostText, HostPortal, - CoroutineComponent, - YieldComponent, + CallComponent, + ReturnComponent, Fragment, } = require('shared/ReactTypeOfWork'); @@ -406,28 +406,24 @@ exports.createFiberFromHostInstanceForDeletion = function(): Fiber { return fiber; }; -exports.createFiberFromCoroutine = function( - coroutine: ReactCoroutine, +exports.createFiberFromCall = function( + call: ReactCall, internalContextTag: TypeOfInternalContext, expirationTime: ExpirationTime, ): Fiber { - const fiber = createFiber( - CoroutineComponent, - coroutine.key, - internalContextTag, - ); - fiber.type = coroutine.handler; - fiber.pendingProps = coroutine; + const fiber = createFiber(CallComponent, call.key, internalContextTag); + fiber.type = call.handler; + fiber.pendingProps = call; fiber.expirationTime = expirationTime; return fiber; }; -exports.createFiberFromYield = function( - yieldNode: ReactYield, +exports.createFiberFromReturn = function( + returnNode: ReactReturn, internalContextTag: TypeOfInternalContext, expirationTime: ExpirationTime, ): Fiber { - const fiber = createFiber(YieldComponent, null, internalContextTag); + const fiber = createFiber(ReturnComponent, null, internalContextTag); fiber.expirationTime = expirationTime; return fiber; }; diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 64a0ce5ee5af8..efeb2a29f2801 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -10,7 +10,7 @@ 'use strict'; import type {HostConfig} from 'react-reconciler'; -import type {ReactCoroutine} from 'shared/ReactTypes'; +import type {ReactCall} from 'shared/ReactTypes'; import type {Fiber} from 'react-reconciler/src/ReactFiber'; import type {HostContext} from './ReactFiberHostContext'; import type {HydrationContext} from './ReactFiberHydrationContext'; @@ -25,9 +25,9 @@ var { HostComponent, HostText, HostPortal, - CoroutineComponent, - CoroutineHandlerPhase, - YieldComponent, + CallComponent, + CallHandlerPhase, + ReturnComponent, Fragment, } = require('shared/ReactTypeOfWork'); var { @@ -537,34 +537,27 @@ module.exports = function( } } - function updateCoroutineComponent( - current, - workInProgress, - renderExpirationTime, - ) { - var nextCoroutine = (workInProgress.pendingProps: null | ReactCoroutine); + function updateCallComponent(current, workInProgress, renderExpirationTime) { + var nextCall = (workInProgress.pendingProps: null | ReactCall); if (hasContextChanged()) { // Normally we can bail out on props equality but if context has changed // we don't do the bailout and we have to reuse existing props instead. - if (nextCoroutine === null) { - nextCoroutine = current && current.memoizedProps; + if (nextCall === null) { + nextCall = current && current.memoizedProps; invariant( - nextCoroutine !== null, + nextCall !== null, 'We should always have pending or current props. This error is ' + 'likely caused by a bug in React. Please file an issue.', ); } - } else if ( - nextCoroutine === null || - workInProgress.memoizedProps === nextCoroutine - ) { - nextCoroutine = workInProgress.memoizedProps; + } else if (nextCall === null || workInProgress.memoizedProps === nextCall) { + nextCall = workInProgress.memoizedProps; // TODO: When bailing out, we might need to return the stateNode instead // of the child. To check it for work. // return bailoutOnAlreadyFinishedWork(current, workInProgress); } - const nextChildren = nextCoroutine.children; + const nextChildren = nextCall.children; // The following is a fork of reconcileChildrenAtExpirationTime but using // stateNode to store the child. @@ -591,7 +584,7 @@ module.exports = function( ); } - memoizeProps(workInProgress, nextCoroutine); + memoizeProps(workInProgress, nextCall); // This doesn't take arbitrary time so we could synchronously just begin // eagerly do the work of workInProgress.child as an optimization. return workInProgress.stateNode; @@ -761,18 +754,18 @@ module.exports = function( ); case HostText: return updateHostText(current, workInProgress); - case CoroutineHandlerPhase: + case CallHandlerPhase: // This is a restart. Reset the tag to the initial phase. - workInProgress.tag = CoroutineComponent; + workInProgress.tag = CallComponent; // Intentionally fall through since this is now the same. - case CoroutineComponent: - return updateCoroutineComponent( + case CallComponent: + return updateCallComponent( current, workInProgress, renderExpirationTime, ); - case YieldComponent: - // A yield component is just a placeholder, we can just run through the + case ReturnComponent: + // A return component is just a placeholder, we can just run through the // next one immediately. return null; case HostPortal: diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 2cabff80375da..bae7e7dece8c6 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -20,7 +20,7 @@ var { HostComponent, HostText, HostPortal, - CoroutineComponent, + CallComponent, } = ReactTypeOfWork; var { invokeGuardedCallback, @@ -217,7 +217,7 @@ module.exports = function( safelyDetachRef(current); return; } - case CoroutineComponent: { + case CallComponent: { commitNestedUnmounts(current.stateNode); return; } diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index df49d1e8dc1b5..e1635828335b4 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -10,7 +10,7 @@ 'use strict'; import type {HostConfig} from 'react-reconciler'; -import type {ReactCoroutine} from 'shared/ReactTypes'; +import type {ReactCall} from 'shared/ReactTypes'; import type {Fiber} from './ReactFiber'; import type {ExpirationTime} from './ReactFiberExpirationTime'; import type {HostContext} from './ReactFiberHostContext'; @@ -26,9 +26,9 @@ var { HostComponent, HostText, HostPortal, - CoroutineComponent, - CoroutineHandlerPhase, - YieldComponent, + CallComponent, + CallHandlerPhase, + ReturnComponent, Fragment, } = require('shared/ReactTypeOfWork'); var {Placement, Ref, Update} = require('shared/ReactTypeOfSideEffect'); @@ -79,7 +79,7 @@ module.exports = function( workInProgress.effectTag |= Ref; } - function appendAllYields(yields: Array, workInProgress: Fiber) { + function appendAllReturns(returns: Array, workInProgress: Fiber) { let node = workInProgress.stateNode; if (node) { node.return = workInProgress; @@ -90,9 +90,9 @@ module.exports = function( node.tag === HostText || node.tag === HostPortal ) { - invariant(false, 'A coroutine cannot have host component children.'); - } else if (node.tag === YieldComponent) { - yields.push(node.type); + invariant(false, 'A call cannot have host component children.'); + } else if (node.tag === ReturnComponent) { + returns.push(node.type); } else if (node.child !== null) { node.child.return = node; node = node.child; @@ -109,34 +109,34 @@ module.exports = function( } } - function moveCoroutineToHandlerPhase( + function moveCallToHandlerPhase( current: Fiber | null, workInProgress: Fiber, renderExpirationTime: ExpirationTime, ) { - var coroutine = (workInProgress.memoizedProps: ?ReactCoroutine); + var call = (workInProgress.memoizedProps: ?ReactCall); invariant( - coroutine, + call, 'Should be resolved by now. This error is likely caused by a bug in ' + 'React. Please file an issue.', ); - // First step of the coroutine has completed. Now we need to do the second. - // TODO: It would be nice to have a multi stage coroutine represented by a + // First step of the call has completed. Now we need to do the second. + // TODO: It would be nice to have a multi stage call represented by a // single component, or at least tail call optimize nested ones. Currently // that requires additional fields that we don't want to add to the fiber. // So this requires nested handlers. // Note: This doesn't mutate the alternate node. I don't think it needs to // since this stage is reset for every pass. - workInProgress.tag = CoroutineHandlerPhase; + workInProgress.tag = CallHandlerPhase; - // Build up the yields. + // Build up the returns. // TODO: Compare this to a generator or opaque helpers like Children. - var yields: Array = []; - appendAllYields(yields, workInProgress); - var fn = coroutine.handler; - var props = coroutine.props; - var nextChildren = fn(props, yields); + var returns: Array = []; + appendAllReturns(returns, workInProgress); + var fn = call.handler; + var props = call.props; + var nextChildren = fn(props, returns); var currentFirstChild = current !== null ? current.child : null; workInProgress.child = reconcileChildFibers( @@ -566,17 +566,17 @@ module.exports = function( } return null; } - case CoroutineComponent: - return moveCoroutineToHandlerPhase( + case CallComponent: + return moveCallToHandlerPhase( current, workInProgress, renderExpirationTime, ); - case CoroutineHandlerPhase: - // Reset the tag to now be a first phase coroutine. - workInProgress.tag = CoroutineComponent; + case CallHandlerPhase: + // Reset the tag to now be a first phase call. + workInProgress.tag = CallComponent; return null; - case YieldComponent: + case ReturnComponent: // Does nothing. return null; case Fragment: diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index 6e084c1b43050..5c5e25a65b760 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -507,7 +507,7 @@ module.exports = function( // Check for pending updates. let newExpirationTime = getUpdateExpirationTime(workInProgress); - // TODO: Coroutines need to visit stateNode + // TODO: Calls need to visit stateNode // Bubble up the earliest expiration time. let child = workInProgress.child; diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalPerf-test.js b/packages/react-reconciler/src/__tests__/ReactIncrementalPerf-test.js index ac751b7ed4cd9..570dd845ee2f1 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncrementalPerf-test.js +++ b/packages/react-reconciler/src/__tests__/ReactIncrementalPerf-test.js @@ -11,7 +11,7 @@ describe('ReactDebugFiberPerf', () => { let React; - let ReactCoroutine; + let ReactCallReturn; let ReactNoop; let ReactPortal; let PropTypes; @@ -115,8 +115,8 @@ describe('ReactDebugFiberPerf', () => { // Import after the polyfill is set up: React = require('react'); ReactNoop = require('react-noop-renderer'); + ReactCallReturn = require('react-call-return'); // TODO: can we express this test with only public API? - ReactCoroutine = require('react-reconciler/src/ReactCoroutine'); ReactPortal = require('react-reconciler/src/ReactPortal'); PropTypes = require('prop-types'); }); @@ -431,13 +431,13 @@ describe('ReactDebugFiberPerf', () => { expect(getFlameChart()).toMatchSnapshot(); }); - it('supports coroutines', () => { + it('supports returns', () => { function Continuation({isSame}) { return ; } function CoChild({bar}) { - return ReactCoroutine.createYield({ + return ReactCallReturn.unstable_createReturn({ props: { bar: bar, }, @@ -449,16 +449,16 @@ describe('ReactDebugFiberPerf', () => { return [, ]; } - function HandleYields(props, yields) { - return yields.map((y, i) => ( + function HandleReturns(props, returns) { + return returns.map((y, i) => ( )); } function CoParent(props) { - return ReactCoroutine.createCoroutine( + return ReactCallReturn.unstable_createCall( props.children, - HandleYields, + HandleReturns, props, ); } diff --git a/packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.js.snap b/packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.js.snap index 9b83f7fd4bb1b..4815a22fc0c02 100644 --- a/packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.js.snap +++ b/packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.js.snap @@ -243,11 +243,11 @@ exports[`ReactDebugFiberPerf skips parents during setState 1`] = ` " `; -exports[`ReactDebugFiberPerf supports coroutines 1`] = ` +exports[`ReactDebugFiberPerf supports returns 1`] = ` "⚛ (React Tree Reconciliation) ⚛ App [mount] ⚛ CoParent [mount] - ⚛ HandleYields [mount] + ⚛ HandleReturns [mount] ⚛ Indirection [mount] ⚛ CoChild [mount] ⚛ CoChild [mount] diff --git a/packages/shared/ReactTypeOfWork.js b/packages/shared/ReactTypeOfWork.js index b5c844bae37df..3b75245b6b6d4 100644 --- a/packages/shared/ReactTypeOfWork.js +++ b/packages/shared/ReactTypeOfWork.js @@ -19,8 +19,8 @@ module.exports = { HostPortal: 4, // A subtree. Could be an entry point to a different renderer. HostComponent: 5, HostText: 6, - CoroutineComponent: 7, - CoroutineHandlerPhase: 8, - YieldComponent: 9, + CallComponent: 7, + CallHandlerPhase: 8, + ReturnComponent: 9, Fragment: 10, }; diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index 5953ba948e2b5..4c672b6075e84 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -11,8 +11,8 @@ export type ReactNode = | React$Element - | ReactCoroutine - | ReactYield + | ReactCall + | ReactReturn | ReactPortal | ReactText | ReactFragment; @@ -25,16 +25,16 @@ export type ReactText = string | number; export type ReactEmpty = null | void | boolean; -export type ReactCoroutine = { +export type ReactCall = { $$typeof: Symbol | number, key: null | string, children: any, - // This should be a more specific CoroutineHandler - handler: (props: any, yields: Array) => ReactNodeList, + // This should be a more specific CallHandler + handler: (props: any, returns: Array) => ReactNodeList, props: any, }; -export type ReactYield = { +export type ReactReturn = { $$typeof: Symbol | number, value: mixed, }; diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index d6445c15f07f8..04889862a92b3 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -196,6 +196,16 @@ const bundles = [ global: 'ReactReconciler', externals: ['react'], }, + + /******* React Call Return (experimental) *******/ + { + label: 'react-call-return', + bundleTypes: [NODE_DEV, NODE_PROD], + moduleType: ISOMORPHIC, + entry: 'react-call-return', + global: 'ReactCallReturn', + externals: [], + }, ]; // Based on deep-freeze by substack (public domain) diff --git a/scripts/rollup/results.json b/scripts/rollup/results.json index 1636646dd6bd4..bb3a9727879e8 100644 --- a/scripts/rollup/results.json +++ b/scripts/rollup/results.json @@ -199,6 +199,14 @@ "react-reconciler.production.min.js (NODE_PROD)": { "size": 38245, "gzip": 11943 + }, + "react-call-return.development.js (NODE_DEV)": { + "size": 2721, + "gzip": 943 + }, + "react-call-return.production.min.js (NODE_PROD)": { + "size": 845, + "gzip": 484 } } } \ No newline at end of file