diff --git a/packages/react-cs-renderer/src/ReactNativeCSFeatureFlags.js b/packages/react-cs-renderer/src/ReactNativeCSFeatureFlags.js new file mode 100644 index 000000000000..874fdf325603 --- /dev/null +++ b/packages/react-cs-renderer/src/ReactNativeCSFeatureFlags.js @@ -0,0 +1,24 @@ +/** + * 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. + * + * @providesModule ReactNativeCSFeatureFlags + * @flow + */ + +'use strict'; + +import type {FeatureFlags} from 'ReactFeatureFlags'; + +var ReactNativeCSFeatureFlags: FeatureFlags = { + enableAsyncSubtreeAPI: true, + enableAsyncSchedulingByDefaultInReactDOM: false, + // React Native CS uses persistent reconciler. + enableMutatingReconciler: false, + enableNoopReconciler: false, + enablePersistentReconciler: true, +}; + +module.exports = ReactNativeCSFeatureFlags; diff --git a/packages/react-cs-renderer/src/ReactNativeCSFiberEntry.js b/packages/react-cs-renderer/src/ReactNativeCSFiberEntry.js index dde53ac20f94..b0cc3bda50ce 100644 --- a/packages/react-cs-renderer/src/ReactNativeCSFiberEntry.js +++ b/packages/react-cs-renderer/src/ReactNativeCSFiberEntry.js @@ -154,34 +154,37 @@ const ReactNativeCSFiberRenderer = ReactFiberReconciler({ persistence: { cloneInstance( instance: Instance, - updatePayload: Object, - type: string, - oldProps: Props, - newProps: Props, - internalInstanceHandle: Object, - keepChildren: boolean, - ): Instance { - return 0; - }, - tryToReuseInstance( - instance: Instance, - updatePayload: Object, + updatePayload: null | Object, type: string, oldProps: Props, newProps: Props, internalInstanceHandle: Object, keepChildren: boolean, + recyclableInstance: null | Instance, ): Instance { return 0; }, - createRootInstance( - rootContainerInstance: Container, - hostContext: {}, - ): Instance { - return 123; + createContainerChildSet( + container: Container, + ): Array { + return []; }, - commitRootInstance(rootInstance: Instance): void {}, + + appendChildToContainerChildSet( + childSet: Array, + child: Instance | TextInstance, + ): void {}, + + finalizeContainerChildren( + container: Container, + newChildren: Array, + ): void {}, + + replaceContainerChildren( + container: Container, + newChildren: Array, + ): void {}, }, }); diff --git a/packages/react-cs-renderer/src/__tests__/ReactNativeCS-test.js b/packages/react-cs-renderer/src/__tests__/ReactNativeCS-test.js index f1ad224d6e5d..3fa811fb255b 100644 --- a/packages/react-cs-renderer/src/__tests__/ReactNativeCS-test.js +++ b/packages/react-cs-renderer/src/__tests__/ReactNativeCS-test.js @@ -12,6 +12,8 @@ var React; var ReactNativeCS; +jest.mock('ReactFeatureFlags', () => require('ReactNativeCSFeatureFlags')); + describe('ReactNativeCS', () => { beforeEach(() => { jest.resetModules(); diff --git a/packages/react-noop-renderer/src/ReactNoopEntry.js b/packages/react-noop-renderer/src/ReactNoopEntry.js index 89bfc33c2d73..f3c7696bdf79 100644 --- a/packages/react-noop-renderer/src/ReactNoopEntry.js +++ b/packages/react-noop-renderer/src/ReactNoopEntry.js @@ -20,6 +20,7 @@ import type {Fiber} from 'ReactFiber'; import type {UpdateQueue} from 'ReactFiberUpdateQueue'; +var ReactFeatureFlags = require('ReactFeatureFlags'); var ReactFiberInstrumentation = require('ReactFiberInstrumentation'); var ReactFiberReconciler = require('ReactFiberReconciler'); var ReactInstanceMap = require('ReactInstanceMap'); @@ -85,7 +86,7 @@ function removeChild( let elapsedTimeInMs = 0; -var NoopRenderer = ReactFiberReconciler({ +var SharedHostConfig = { getRootHostContext() { if (failInBeginPhase) { throw new Error('Error in host config.'); @@ -176,7 +177,10 @@ var NoopRenderer = ReactFiberReconciler({ now(): number { return elapsedTimeInMs; }, +}; +var NoopRenderer = ReactFiberReconciler({ + ...SharedHostConfig, mutation: { commitMount(instance: Instance, type: string, newProps: Props): void { // Noop @@ -211,8 +215,64 @@ var NoopRenderer = ReactFiberReconciler({ }, }); +var PersistentNoopRenderer = ReactFeatureFlags.enablePersistentReconciler + ? ReactFiberReconciler({ + ...SharedHostConfig, + persistence: { + cloneInstance( + instance: Instance, + updatePayload: null | Object, + type: string, + oldProps: Props, + newProps: Props, + internalInstanceHandle: Object, + keepChildren: boolean, + recyclableInstance: null | Instance, + ): Instance { + const clone = { + id: instance.id, + type: type, + children: keepChildren ? instance.children : [], + prop: newProps.prop, + }; + Object.defineProperty(clone, 'id', { + value: clone.id, + enumerable: false, + }); + return clone; + }, + + createContainerChildSet( + container: Container, + ): Array { + return []; + }, + + appendChildToContainerChildSet( + childSet: Array, + child: Instance | TextInstance, + ): void { + childSet.push(child); + }, + + finalizeContainerChildren( + container: Container, + newChildren: Array, + ): void {}, + + replaceContainerChildren( + container: Container, + newChildren: Array, + ): void { + container.children = newChildren; + }, + }, + }) + : null; + var rootContainers = new Map(); var roots = new Map(); +var persistentRoots = new Map(); var DEFAULT_ROOT_ID = ''; let yieldedValues = null; @@ -275,6 +335,26 @@ var ReactNoop = { NoopRenderer.updateContainer(element, root, null, callback); }, + renderToPersistentRootWithID( + element: React$Element, + rootID: string, + callback: ?Function, + ) { + if (PersistentNoopRenderer === null) { + throw new Error( + 'Enable ReactFeatureFlags.enablePersistentReconciler to use it in tests.', + ); + } + let root = persistentRoots.get(rootID); + if (!root) { + const container = {rootID: rootID, children: []}; + rootContainers.set(rootID, container); + root = PersistentNoopRenderer.createContainer(container, false); + persistentRoots.set(rootID, root); + } + PersistentNoopRenderer.updateContainer(element, root, null, callback); + }, + unmountRootWithID(rootID: string) { const root = roots.get(rootID); if (root) { diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index 633911db3413..18e4a984a3f3 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -445,6 +445,7 @@ exports.createFiberFromPortal = function( fiber.expirationTime = expirationTime; fiber.stateNode = { containerInfo: portal.containerInfo, + pendingChildren: null, // Used by persistent updates implementation: portal.implementation, }; return fiber; diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 508a41a86226..ff7f992ef60b 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -67,8 +67,8 @@ if (__DEV__) { var warnedAboutStatelessRefs = {}; } -module.exports = function( - config: HostConfig, +module.exports = function( + config: HostConfig, hostContext: HostContext, hydrationContext: HydrationContext, scheduleWork: (fiber: Fiber, expirationTime: ExpirationTime) => void, diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 1cf90d93e30f..8d661e63dc1d 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -13,6 +13,7 @@ import type {Fiber} from 'ReactFiber'; import type {HostConfig} from 'ReactFiberReconciler'; +var ReactFeatureFlags = require('ReactFeatureFlags'); var ReactTypeOfWork = require('ReactTypeOfWork'); var { ClassComponent, @@ -38,11 +39,11 @@ if (__DEV__) { var {startPhaseTimer, stopPhaseTimer} = require('ReactDebugFiberPerf'); } -module.exports = function( - config: HostConfig, +module.exports = function( + config: HostConfig, captureError: (failedFiber: Fiber, error: mixed) => Fiber | null, ) { - const {getPublicInstance} = config; + const {getPublicInstance, mutation, persistence} = config; if (__DEV__) { var callComponentWillUnmountWithTimerInDev = function(current, instance) { @@ -162,6 +163,10 @@ module.exports = function( // We have no life-cycles associated with text. return; } + case HostPortal: { + // We have no life-cycles associated with portals. + return; + } default: { invariant( false, @@ -222,7 +227,14 @@ module.exports = function( // TODO: this is recursive. // We are also not using this parent because // the portal will get pushed immediately. - unmountHostComponents(current); + if (ReactFeatureFlags.enableMutatingReconciler && mutation) { + unmountHostComponents(current); + } else if ( + ReactFeatureFlags.enablePersistentReconciler && + persistence + ) { + emptyPortalContainer(current); + } return; } } @@ -239,7 +251,12 @@ module.exports = function( commitUnmount(node); // Visit children because they may contain more composite or host nodes. // Skip portals because commitUnmount() currently visits them recursively. - if (node.child !== null && node.tag !== HostPortal) { + if ( + node.child !== null && + // If we use mutation we drill down into portals using commitUnmount above. + // If we don't use mutation we drill down into portals here instead. + (!mutation || node.tag !== HostPortal) + ) { node.child.return = node; node = node.child; continue; @@ -272,22 +289,75 @@ module.exports = function( } } - if (!config.mutation) { - return { - commitResetTextContent(finishedWork: Fiber) {}, - commitPlacement(finishedWork: Fiber) {}, - commitDeletion(current: Fiber) { - // Detach refs and call componentWillUnmount() on the whole subtree. - commitNestedUnmounts(current); - detachFiber(current); - }, - commitWork(current: Fiber | null, finishedWork: Fiber) {}, - commitLifeCycles, - commitAttachRef, - commitDetachRef, - }; + if (!mutation) { + let commitContainer; + if (persistence) { + const {replaceContainerChildren, createContainerChildSet} = persistence; + var emptyPortalContainer = function(current: Fiber) { + const portal: {containerInfo: C, pendingChildren: CC} = + current.stateNode; + const {containerInfo} = portal; + const emptyChildSet = createContainerChildSet(containerInfo); + replaceContainerChildren(containerInfo, emptyChildSet); + }; + commitContainer = function(finishedWork: Fiber) { + switch (finishedWork.tag) { + case ClassComponent: { + return; + } + case HostComponent: { + return; + } + case HostText: { + return; + } + case HostRoot: + case HostPortal: { + const portalOrRoot: {containerInfo: C, pendingChildren: CC} = + finishedWork.stateNode; + const {containerInfo, pendingChildren} = portalOrRoot; + replaceContainerChildren(containerInfo, pendingChildren); + return; + } + default: { + invariant( + false, + 'This unit of work tag should not have side-effects. This error is ' + + 'likely caused by a bug in React. Please file an issue.', + ); + } + } + }; + } else { + commitContainer = function(finishedWork: Fiber) { + // Noop + }; + } + if ( + ReactFeatureFlags.enablePersistentReconciler || + ReactFeatureFlags.enableNoopReconciler + ) { + return { + commitResetTextContent(finishedWork: Fiber) {}, + commitPlacement(finishedWork: Fiber) {}, + commitDeletion(current: Fiber) { + // Detach refs and call componentWillUnmount() on the whole subtree. + commitNestedUnmounts(current); + detachFiber(current); + }, + commitWork(current: Fiber | null, finishedWork: Fiber) { + commitContainer(finishedWork); + }, + commitLifeCycles, + commitAttachRef, + commitDetachRef, + }; + } else if (persistence) { + invariant(false, 'Persistent reconciler is disabled.'); + } else { + invariant(false, 'Noop reconciler is disabled.'); + } } - const { commitMount, commitUpdate, @@ -299,7 +369,7 @@ module.exports = function( insertInContainerBefore, removeChild, removeChildFromContainer, - } = config.mutation; + } = mutation; function getHostParentFiber(fiber: Fiber): Fiber { let parent = fiber.return; @@ -599,13 +669,17 @@ module.exports = function( resetTextContent(current.stateNode); } - return { - commitResetTextContent, - commitPlacement, - commitDeletion, - commitWork, - commitLifeCycles, - commitAttachRef, - commitDetachRef, - }; + if (ReactFeatureFlags.enableMutatingReconciler) { + return { + commitResetTextContent, + commitPlacement, + commitDeletion, + commitWork, + commitLifeCycles, + commitAttachRef, + commitDetachRef, + }; + } else { + invariant(false, 'Mutating reconciler is disabled.'); + } }; diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index afc3b5d52b8a..5aff0892c761 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -23,6 +23,7 @@ var { popContextProvider, popTopLevelContextObject, } = require('ReactFiberContext'); +var ReactFeatureFlags = require('ReactFeatureFlags'); var ReactTypeOfWork = require('ReactTypeOfWork'); var ReactTypeOfSideEffect = require('ReactTypeOfSideEffect'); var ReactFiberExpirationTime = require('ReactFiberExpirationTime'); @@ -44,8 +45,8 @@ var {Never} = ReactFiberExpirationTime; var invariant = require('fbjs/lib/invariant'); -module.exports = function( - config: HostConfig, +module.exports = function( + config: HostConfig, hostContext: HostContext, hydrationContext: HydrationContext, ) { @@ -55,6 +56,8 @@ module.exports = function( appendInitialChild, finalizeInitialChildren, prepareUpdate, + mutation, + persistence, } = config; const { @@ -161,6 +164,7 @@ module.exports = function( // down its children. Instead, we'll get insertions from each child in // the portal directly. } else if (node.child !== null) { + node.child.return = node; node = node.child; continue; } @@ -173,10 +177,218 @@ module.exports = function( } node = node.return; } + node.sibling.return = node.return; node = node.sibling; } } + let updateHostContainer; + let updateHostComponent; + let updateHostText; + if (mutation) { + if (ReactFeatureFlags.enableMutatingReconciler) { + // Mutation mode + updateHostContainer = function(workInProgress: Fiber) { + // Noop + }; + updateHostComponent = function( + current: Fiber, + workInProgress: Fiber, + updatePayload: null | PL, + type: T, + oldProps: P, + newProps: P, + rootContainerInstance: C, + ) { + // TODO: Type this specific to this type of component. + workInProgress.updateQueue = (updatePayload: any); + // If the update payload indicates that there is a change or if there + // is a new ref we mark this as an update. All the work is done in commitWork. + if (updatePayload) { + markUpdate(workInProgress); + } + }; + updateHostText = function( + current: Fiber, + workInProgress: Fiber, + oldText: string, + newText: string, + ) { + // If the text differs, mark it as an update. All the work in done in commitWork. + if (oldText !== newText) { + markUpdate(workInProgress); + } + }; + } else { + invariant(false, 'Mutating reconciler is disabled.'); + } + } else if (persistence) { + if (ReactFeatureFlags.enablePersistentReconciler) { + // Persistent host tree mode + const { + cloneInstance, + createContainerChildSet, + appendChildToContainerChildSet, + finalizeContainerChildren, + } = persistence; + + // An unfortunate fork of appendAllChildren because we have two different parent types. + const appendAllChildrenToContainer = function( + containerChildSet: CC, + workInProgress: Fiber, + ) { + // We only have the top Fiber that was created but we need recurse down its + // children to find all the terminal nodes. + let node = workInProgress.child; + while (node !== null) { + if (node.tag === HostComponent || node.tag === HostText) { + appendChildToContainerChildSet(containerChildSet, node.stateNode); + } else if (node.tag === HostPortal) { + // If we have a portal child, then we don't want to traverse + // down its children. Instead, we'll get insertions from each child in + // the portal directly. + } else if (node.child !== null) { + node.child.return = node; + node = node.child; + continue; + } + if (node === workInProgress) { + return; + } + while (node.sibling === null) { + if (node.return === null || node.return === workInProgress) { + return; + } + node = node.return; + } + node.sibling.return = node.return; + node = node.sibling; + } + }; + updateHostContainer = function(workInProgress: Fiber) { + const portalOrRoot: {containerInfo: C, pendingChildren: CC} = + workInProgress.stateNode; + const childrenUnchanged = workInProgress.firstEffect === null; + if (childrenUnchanged) { + // No changes, just reuse the existing instance. + } else { + const container = portalOrRoot.containerInfo; + let newChildSet = createContainerChildSet(container); + if (finalizeContainerChildren(container, newChildSet)) { + markUpdate(workInProgress); + } + portalOrRoot.pendingChildren = newChildSet; + // If children might have changed, we have to add them all to the set. + appendAllChildrenToContainer(newChildSet, workInProgress); + // Schedule an update on the container to swap out the container. + markUpdate(workInProgress); + } + }; + updateHostComponent = function( + current: Fiber, + workInProgress: Fiber, + updatePayload: null | PL, + type: T, + oldProps: P, + newProps: P, + rootContainerInstance: C, + ) { + // If there are no effects associated with this node, then none of our children had any updates. + // This guarantees that we can reuse all of them. + const childrenUnchanged = workInProgress.firstEffect === null; + const currentInstance = current.stateNode; + if (childrenUnchanged && updatePayload === null) { + // No changes, just reuse the existing instance. + // Note that this might release a previous clone. + workInProgress.stateNode = currentInstance; + } else { + let recyclableInstance = workInProgress.stateNode; + let newInstance = cloneInstance( + currentInstance, + updatePayload, + type, + oldProps, + newProps, + workInProgress, + childrenUnchanged, + recyclableInstance, + ); + if ( + finalizeInitialChildren( + newInstance, + type, + newProps, + rootContainerInstance, + ) + ) { + markUpdate(workInProgress); + } + workInProgress.stateNode = newInstance; + if (childrenUnchanged) { + // If there are no other effects in this tree, we need to flag this node as having one. + // Even though we're not going to use it for anything. + // Otherwise parents won't know that there are new children to propagate upwards. + markUpdate(workInProgress); + } else { + // If children might have changed, we have to add them all to the set. + appendAllChildren(newInstance, workInProgress); + } + } + }; + updateHostText = function( + current: Fiber, + workInProgress: Fiber, + oldText: string, + newText: string, + ) { + if (oldText !== newText) { + // If the text content differs, we'll create a new text instance for it. + const rootContainerInstance = getRootHostContainer(); + const currentHostContext = getHostContext(); + workInProgress.stateNode = createTextInstance( + newText, + rootContainerInstance, + currentHostContext, + workInProgress, + ); + // We'll have to mark it as having an effect, even though we won't use the effect for anything. + // This lets the parents know that at least one of their children has changed. + markUpdate(workInProgress); + } + }; + } else { + invariant(false, 'Persistent reconciler is disabled.'); + } + } else { + if (ReactFeatureFlags.enableNoopReconciler) { + // No host operations + updateHostContainer = function(workInProgress: Fiber) { + // Noop + }; + updateHostComponent = function( + current: Fiber, + workInProgress: Fiber, + updatePayload: null | PL, + type: T, + oldProps: P, + newProps: P, + rootContainerInstance: C, + ) { + // Noop + }; + updateHostText = function( + current: Fiber, + workInProgress: Fiber, + oldText: string, + newText: string, + ) { + // Noop + }; + } else { + invariant(false, 'Noop reconciler is disabled.'); + } + } + function completeWork( current: Fiber | null, workInProgress: Fiber, @@ -219,6 +431,7 @@ module.exports = function( // TODO: Delete this when we delete isMounted and findDOMNode. workInProgress.effectTag &= ~Placement; } + updateHostContainer(workInProgress); return null; } case HostComponent: { @@ -244,13 +457,16 @@ module.exports = function( currentHostContext, ); - // TODO: Type this specific to this type of component. - workInProgress.updateQueue = (updatePayload: any); - // If the update payload indicates that there is a change or if there - // is a new ref we mark this as an update. - if (updatePayload) { - markUpdate(workInProgress); - } + updateHostComponent( + current, + workInProgress, + updatePayload, + type, + oldProps, + newProps, + rootContainerInstance, + ); + if (current.ref !== workInProgress.ref) { markRef(workInProgress); } @@ -325,9 +541,7 @@ module.exports = function( const oldText = current.memoizedProps; // If we have an alternate, that means this is an update and we need // to schedule a side-effect to do the updates. - if (oldText !== newText) { - markUpdate(workInProgress); - } + updateHostText(current, workInProgress, oldText, newText); } else { if (typeof newText !== 'string') { invariant( @@ -373,6 +587,7 @@ module.exports = function( return null; case HostPortal: popHostContainer(workInProgress); + updateHostContainer(workInProgress); return null; // Error cases case IndeterminateComponent: diff --git a/packages/react-reconciler/src/ReactFiberHostContext.js b/packages/react-reconciler/src/ReactFiberHostContext.js index dfd7f0e334d6..657f60280ea2 100644 --- a/packages/react-reconciler/src/ReactFiberHostContext.js +++ b/packages/react-reconciler/src/ReactFiberHostContext.js @@ -31,8 +31,8 @@ export type HostContext = { resetHostContainer(): void, }; -module.exports = function( - config: HostConfig, +module.exports = function( + config: HostConfig, ): HostContext { const {getChildHostContext, getRootHostContext} = config; diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.js b/packages/react-reconciler/src/ReactFiberHydrationContext.js index f6024e39c671..d710db067197 100644 --- a/packages/react-reconciler/src/ReactFiberHydrationContext.js +++ b/packages/react-reconciler/src/ReactFiberHydrationContext.js @@ -33,8 +33,8 @@ export type HydrationContext = { popHydrationState(fiber: Fiber): boolean, }; -module.exports = function( - config: HostConfig, +module.exports = function( + config: HostConfig, ): HydrationContext { const {shouldSetTextContent, hydration} = config; diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js index 7f6a7c3017c3..4999d73905f3 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.js @@ -47,7 +47,7 @@ export type Deadline = { type OpaqueHandle = Fiber; type OpaqueRoot = FiberRoot; -export type HostConfig = { +export type HostConfig = { getRootHostContext(rootContainerInstance: C): CX, getChildHostContext(parentHostContext: CX, type: T, instance: C): CX, getPublicInstance(instance: I | TI): PI, @@ -100,7 +100,7 @@ export type HostConfig = { +hydration?: HydrationHostConfig, +mutation?: MutableUpdatesHostConfig, - +persistence?: PersistentUpdatesHostConfig, + +persistence?: PersistentUpdatesHostConfig, }; type MutableUpdatesHostConfig = { @@ -132,28 +132,24 @@ type MutableUpdatesHostConfig = { removeChildFromContainer(container: C, child: I | TI): void, }; -type PersistentUpdatesHostConfig = { +type PersistentUpdatesHostConfig = { cloneInstance( instance: I, - updatePayload: PL, - type: T, - oldProps: P, - newProps: P, - internalInstanceHandle: OpaqueHandle, - keepChildren: boolean, - ): I, - tryToReuseInstance( - instance: I, - updatePayload: PL, + updatePayload: null | PL, type: T, oldProps: P, newProps: P, internalInstanceHandle: OpaqueHandle, keepChildren: boolean, + recyclableInstance: I, ): I, - createRootInstance(rootContainerInstance: C, hostContext: CX): I, - commitRootInstance(rootInstance: I): void, + createContainerChildSet(container: C): CC, + + appendChildToContainerChildSet(childSet: CC, child: I | TI): void, + finalizeContainerChildren(container: C, newChildren: CC): void, + + replaceContainerChildren(container: C, newChildren: CC): void, }; type HydrationHostConfig = { @@ -257,8 +253,8 @@ function getContextForSubtree( : parentContext; } -module.exports = function( - config: HostConfig, +module.exports = function( + config: HostConfig, ): Reconciler { var {getPublicInstance} = config; diff --git a/packages/react-reconciler/src/ReactFiberRoot.js b/packages/react-reconciler/src/ReactFiberRoot.js index 419e842e08be..1288fee9bb81 100644 --- a/packages/react-reconciler/src/ReactFiberRoot.js +++ b/packages/react-reconciler/src/ReactFiberRoot.js @@ -17,6 +17,8 @@ const {createHostRootFiber} = require('ReactFiber'); export type FiberRoot = { // Any additional information from the host associated with this root. containerInfo: any, + // Used only by persistent updates. + pendingChildren: any, // The currently active root fiber. This is the mutable root of the tree. current: Fiber, // Determines if this root has already been added to the schedule for work. @@ -40,6 +42,7 @@ exports.createFiberRoot = function( const root = { current: uninitializedFiber, containerInfo: containerInfo, + pendingChildren: null, isScheduled: false, nextScheduledRoot: null, context: null, diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index 50f16402d066..45e335a72f19 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -153,8 +153,8 @@ if (__DEV__) { var timeHeuristicForUnitOfWork = 1; -module.exports = function( - config: HostConfig, +module.exports = function( + config: HostConfig, ) { const hostContext = ReactFiberHostContext(config); const hydrationContext: HydrationContext = ReactFiberHydrationContext( diff --git a/packages/react-reconciler/src/__tests__/ReactPersistent-test.js b/packages/react-reconciler/src/__tests__/ReactPersistent-test.js new file mode 100644 index 000000000000..023050b51977 --- /dev/null +++ b/packages/react-reconciler/src/__tests__/ReactPersistent-test.js @@ -0,0 +1,207 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails react-core + */ + +'use strict'; + +var React; +var ReactNoop; +let ReactPortal; + +describe('ReactPersistent', () => { + beforeEach(() => { + jest.resetModules(); + + const ReactFeatureFlags = require('ReactFeatureFlags'); + ReactFeatureFlags.enableMutableReconciler = false; + ReactFeatureFlags.enablePersistentReconciler = true; + ReactFeatureFlags.enableNoopReconciler = false; + + React = require('react'); + ReactNoop = require('react-noop-renderer'); + ReactPortal = require('ReactPortal'); + }); + + const DEFAULT_ROOT_ID = 'persistent-test'; + + function render(element) { + ReactNoop.renderToPersistentRootWithID(element, DEFAULT_ROOT_ID); + } + + function div(...children) { + children = children.map(c => (typeof c === 'string' ? {text: c} : c)); + return {type: 'div', children, prop: undefined}; + } + + function span(prop) { + return {type: 'span', children: [], prop}; + } + + function getChildren() { + return ReactNoop.getChildren(DEFAULT_ROOT_ID); + } + + it('can update child nodes of a host instance', () => { + function Bar(props) { + return {props.text}; + } + + function Foo(props) { + return ( +
+ + {props.text === 'World' ? : null} +
+ ); + } + + render(); + ReactNoop.flush(); + var originalChildren = getChildren(); + expect(originalChildren).toEqual([div(span())]); + + render(); + ReactNoop.flush(); + var newChildren = getChildren(); + expect(newChildren).toEqual([div(span(), span())]); + + expect(originalChildren).toEqual([div(span())]); + }); + + it('can reuse child nodes between updates', () => { + function Baz(props) { + return ; + } + class Bar extends React.Component { + shouldComponentUpdate(newProps) { + return false; + } + render() { + return ; + } + } + function Foo(props) { + return ( +
+ + {props.text === 'World' ? : null} +
+ ); + } + + render(); + ReactNoop.flush(); + var originalChildren = getChildren(); + expect(originalChildren).toEqual([div(span('Hello'))]); + + render(); + ReactNoop.flush(); + var newChildren = getChildren(); + expect(newChildren).toEqual([div(span('Hello'), span('World'))]); + + expect(originalChildren).toEqual([div(span('Hello'))]); + + // Reused node should have reference equality + expect(newChildren[0].children[0]).toBe(originalChildren[0].children[0]); + }); + + it('can update child text nodes', () => { + function Foo(props) { + return ( +
+ {props.text} + +
+ ); + } + + render(); + ReactNoop.flush(); + var originalChildren = getChildren(); + expect(originalChildren).toEqual([div('Hello', span())]); + + render(); + ReactNoop.flush(); + var newChildren = getChildren(); + expect(newChildren).toEqual([div('World', span())]); + + expect(originalChildren).toEqual([div('Hello', span())]); + }); + + it('supports portals', () => { + function Parent(props) { + return
{props.children}
; + } + + function BailoutSpan() { + return ; + } + + class BailoutTest extends React.Component { + shouldComponentUpdate() { + return false; + } + render() { + return ; + } + } + + function Child(props) { + return
{props.children}
; + } + const portalContainer = {rootID: 'persistent-portal-test', children: []}; + const emptyPortalChildSet = portalContainer.children; + render( + + {ReactPortal.createPortal(, portalContainer, null)} + , + ); + ReactNoop.flush(); + + expect(emptyPortalChildSet).toEqual([]); + + var originalChildren = getChildren(); + expect(originalChildren).toEqual([div()]); + var originalPortalChildren = portalContainer.children; + expect(originalPortalChildren).toEqual([div(span())]); + + render( + + {ReactPortal.createPortal( + Hello {'World'}, + portalContainer, + null, + )} + , + ); + ReactNoop.flush(); + + var newChildren = getChildren(); + expect(newChildren).toEqual([div()]); + var newPortalChildren = portalContainer.children; + expect(newPortalChildren).toEqual([div(span(), 'Hello ', 'World')]); + + expect(originalChildren).toEqual([div()]); + expect(originalPortalChildren).toEqual([div(span())]); + + // Reused portal children should have reference equality + expect(newPortalChildren[0].children[0]).toBe( + originalPortalChildren[0].children[0], + ); + + // Deleting the Portal, should clear its children + render(); + ReactNoop.flush(); + + var clearedPortalChildren = portalContainer.children; + expect(clearedPortalChildren).toEqual([]); + + // The original is unchanged. + expect(newPortalChildren).toEqual([div(span(), 'Hello ', 'World')]); + }); +}); diff --git a/packages/shared/src/utils/ReactFeatureFlags.js b/packages/shared/src/utils/ReactFeatureFlags.js index d60ef12f551a..6291d1054abc 100644 --- a/packages/shared/src/utils/ReactFeatureFlags.js +++ b/packages/shared/src/utils/ReactFeatureFlags.js @@ -13,11 +13,20 @@ export type FeatureFlags = {| enableAsyncSubtreeAPI: boolean, enableAsyncSchedulingByDefaultInReactDOM: boolean, + enableMutatingReconciler: boolean, + enableNoopReconciler: boolean, + enablePersistentReconciler: boolean, |}; var ReactFeatureFlags: FeatureFlags = { enableAsyncSubtreeAPI: true, enableAsyncSchedulingByDefaultInReactDOM: false, + // Mutating mode (React DOM, React ART, React Native): + enableMutatingReconciler: true, + // Experimental noop mode (currently unused): + enableNoopReconciler: false, + // Experimental persistent mode (CS): + enablePersistentReconciler: false, }; if (__DEV__) { diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index cd9a6c83dc8d..b4045c0640e0 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -233,5 +233,12 @@ "231": "Expected `%s` listener to be a function, instead got a value of `%s` type.", "232": "_processChildContext is not available in React 16+. This likely means you have multiple copies of React and are attempting to nest a React 15 tree inside a React 16 tree using unstable_renderSubtreeIntoContainer, which isn't supported. Try to make sure you have only one copy of React (and ideally, switch to ReactDOM.createPortal).", "233": "Unsupported top level event type \"%s\" dispatched", - "234": "Event cannot be both direct and bubbling: %s" + "234": "Event cannot be both direct and bubbling: %s", + "235": "Persistent reconciler is disabled.", + "236": "Noop reconciler is disabled.", + "237": "Mutating reconciler is disabled.", + "238": "Task updates can only be scheduled as a nested update or inside batchedUpdates. This error is likely caused by a bug in React. Please file an issue.", + "239": "Measure not implemented yet", + "240": "Text components are not supported for now.", + "241": "Text components are not yet supported." } diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index 01581f73abcc..7868ce0f01ba 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -323,6 +323,7 @@ const bundles = [ label: 'native-cs-fiber', manglePropertiesOnProd: false, name: 'react-native-cs-renderer', + featureFlags: 'packages/react-cs-renderer/src/ReactNativeCSFeatureFlags', paths: [ 'packages/react-native-renderer/**/*.js', // This is used since we reuse the error dialog code 'packages/react-cs-renderer/**/*.js', diff --git a/scripts/rollup/results.json b/scripts/rollup/results.json index 0a5ce382a14b..018169ca02a0 100644 --- a/scripts/rollup/results.json +++ b/scripts/rollup/results.json @@ -25,28 +25,28 @@ "gzip": 6709 }, "react-dom.development.js (UMD_DEV)": { - "size": 622438, - "gzip": 143476 + "size": 632002, + "gzip": 144937 }, "react-dom.production.min.js (UMD_PROD)": { - "size": 100793, - "gzip": 31792 + "size": 100474, + "gzip": 31680 }, "react-dom.development.js (NODE_DEV)": { - "size": 584733, - "gzip": 134780 + "size": 594283, + "gzip": 136223 }, "react-dom.production.min.js (NODE_PROD)": { - "size": 105201, - "gzip": 33053 + "size": 107016, + "gzip": 33539 }, "ReactDOMFiber-dev.js (FB_DEV)": { - "size": 581810, - "gzip": 134178 + "size": 591222, + "gzip": 135569 }, "ReactDOMFiber-prod.js (FB_PROD)": { - "size": 413106, - "gzip": 92244 + "size": 421158, + "gzip": 93551 }, "react-dom-test-utils.development.js (NODE_DEV)": { "size": 41688, @@ -113,44 +113,44 @@ "gzip": 6214 }, "react-art.development.js (UMD_DEV)": { - "size": 368779, - "gzip": 81552 + "size": 378343, + "gzip": 83009 }, "react-art.production.min.js (UMD_PROD)": { - "size": 82740, - "gzip": 25652 + "size": 82432, + "gzip": 25592 }, "react-art.development.js (NODE_DEV)": { - "size": 293112, - "gzip": 62358 + "size": 302698, + "gzip": 63826 }, "react-art.production.min.js (NODE_PROD)": { - "size": 52107, - "gzip": 16388 + "size": 53897, + "gzip": 16869 }, "ReactARTFiber-dev.js (FB_DEV)": { - "size": 291853, - "gzip": 62333 + "size": 301301, + "gzip": 63733 }, "ReactARTFiber-prod.js (FB_PROD)": { - "size": 217374, - "gzip": 45136 + "size": 225462, + "gzip": 46449 }, "ReactNativeFiber-dev.js (RN_DEV)": { - "size": 278956, - "gzip": 48430 + "size": 285864, + "gzip": 49285 }, "ReactNativeFiber-prod.js (RN_PROD)": { - "size": 217084, - "gzip": 37619 + "size": 223648, + "gzip": 38454 }, "react-test-renderer.development.js (NODE_DEV)": { - "size": 296835, - "gzip": 62765 + "size": 306386, + "gzip": 64218 }, "ReactTestRendererFiber-dev.js (FB_DEV)": { - "size": 295566, - "gzip": 62732 + "size": 304979, + "gzip": 64135 }, "react-test-renderer-shallow.development.js (NODE_DEV)": { "size": 9370, @@ -161,8 +161,8 @@ "gzip": 2253 }, "react-noop-renderer.development.js (NODE_DEV)": { - "size": 284424, - "gzip": 59683 + "size": 295594, + "gzip": 61405 }, "react-dom-server.development.js (UMD_DEV)": { "size": 120897, @@ -189,16 +189,16 @@ "gzip": 7520 }, "ReactNativeRTFiber-dev.js (RN_DEV)": { - "size": 210860, - "gzip": 35891 + "size": 217767, + "gzip": 36738 }, "ReactNativeRTFiber-prod.js (RN_PROD)": { - "size": 158763, - "gzip": 26631 + "size": 165326, + "gzip": 27464 }, "react-test-renderer.production.min.js (NODE_PROD)": { - "size": 53651, - "gzip": 16653 + "size": 55449, + "gzip": 17169 }, "react-test-renderer-shallow.production.min.js (NODE_PROD)": { "size": 4630, @@ -209,20 +209,20 @@ "gzip": 4241 }, "react-reconciler.development.js (NODE_DEV)": { - "size": 271721, - "gzip": 56840 + "size": 281271, + "gzip": 58283 }, "react-reconciler.production.min.js (NODE_PROD)": { - "size": 37669, - "gzip": 11733 + "size": 37658, + "gzip": 11762 }, "ReactNativeCSFiber-dev.js (RN_DEV)": { - "size": 203248, - "gzip": 34141 + "size": 210188, + "gzip": 34969 }, "ReactNativeCSFiber-prod.js (RN_PROD)": { - "size": 153658, - "gzip": 25401 + "size": 160321, + "gzip": 26276 } } } \ No newline at end of file