diff --git a/.flowconfig b/.flowconfig index 94ffcb54a2..d15db1c9ab 100644 --- a/.flowconfig +++ b/.flowconfig @@ -34,4 +34,4 @@ suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(2[0-4]\\|1[0-9]\\|[0-9 suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy [version] -^0.26.0 +^0.27.0 diff --git a/package.json b/package.json index afa8ed8947..67ac99e5f8 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "eslint-plugin-react-internal": "file:eslint-rules", "fbjs": "^0.8.1", "fbjs-scripts": "^0.6.0", - "flow-bin": "^0.26.0", + "flow-bin": "^0.27.0", "glob": "^6.0.1", "grunt": "^0.4.5", "grunt-cli": "^0.1.13", diff --git a/src/renderers/noop/ReactNoop.js b/src/renderers/noop/ReactNoop.js index 4ffa0b6582..5682dc9f8a 100644 --- a/src/renderers/noop/ReactNoop.js +++ b/src/renderers/noop/ReactNoop.js @@ -27,18 +27,53 @@ var ReactFiberReconciler = require('ReactFiberReconciler'); var scheduledHighPriCallback = null; var scheduledLowPriCallback = null; +const TERMINAL_TAG = 99; + +type Container = { rootID: number, children: Array }; type Props = { }; -type Instance = { id: number }; +type Instance = { tag: 99, type: string, id: number, children: Array }; var instanceCounter = 0; +function recursivelyAppendChildren(flatArray : Array, child : HostChildren) { + if (!child) { + return; + } + if (child.tag === TERMINAL_TAG) { + flatArray.push(child); + } else { + let node = child; + do { + recursivelyAppendChildren(flatArray, node.output); + } while (node = node.sibling); + } +} + +function flattenChildren(children : HostChildren) { + const flatArray = []; + recursivelyAppendChildren(flatArray, children); + return flatArray; +} + var NoopRenderer = ReactFiberReconciler({ + updateContainer(containerInfo : Container, children : HostChildren) : void { + console.log('Update container #' + containerInfo.rootID); + containerInfo.children = flattenChildren(children); + }, + createInstance(type : string, props : Props, children : HostChildren) : Instance { console.log('Create instance #' + instanceCounter); - return { - id: instanceCounter++ + const inst = { + tag: TERMINAL_TAG, + id: instanceCounter++, + type: type, + children: flattenChildren(children), }; + // Hide from unit tests + Object.defineProperty(inst, 'tag', { value: inst.tag, enumerable: false }); + Object.defineProperty(inst, 'id', { value: inst.id, enumerable: false }); + return inst; }, prepareUpdate(instance : Instance, oldProps : Props, newProps : Props, children : HostChildren) : boolean { @@ -48,6 +83,7 @@ var NoopRenderer = ReactFiberReconciler({ commitUpdate(instance : Instance, oldProps : Props, newProps : Props, children : HostChildren) : void { console.log('Commit update on #' + instance.id); + instance.children = flattenChildren(children); }, deleteInstance(instance : Instance) : void { @@ -64,13 +100,17 @@ var NoopRenderer = ReactFiberReconciler({ }); +var rootContainer = { rootID: 0, children: [] }; + var root = null; var ReactNoop = { + root: rootContainer, + render(element : ReactElement) { if (!root) { - root = NoopRenderer.mountContainer(element, null); + root = NoopRenderer.mountContainer(element, rootContainer); } else { NoopRenderer.updateContainer(element, root); } @@ -115,6 +155,19 @@ var ReactNoop = { console.log('Nothing rendered yet.'); return; } + + function logHostInstances(children: Array, depth) { + for (var i = 0; i < children.length; i++) { + var child = children[i]; + console.log(' '.repeat(depth) + '- ' + child.type + '#' + child.id); + logHostInstances(child.children, depth + 1); + } + } + function logContainer(container : Container, depth) { + console.log(' '.repeat(depth) + '- [root#' + container.rootID + ']'); + logHostInstances(container.children, depth + 1); + } + function logFiber(fiber : Fiber, depth) { console.log(' '.repeat(depth) + '- ' + (fiber.type ? fiber.type.name || fiber.type : '[root]'), '[' + fiber.pendingWorkPriority + (fiber.pendingProps ? '*' : '') + ']'); if (fiber.child) { @@ -124,6 +177,10 @@ var ReactNoop = { logFiber(fiber.sibling, depth); } } + + console.log('HOST INSTANCES:'); + logContainer(rootContainer, 0); + console.log('FIBERS:'); logFiber((root.stateNode : any).current, 0); }, diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index 4de8ce2d5b..4fcf4d87b0 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -34,7 +34,7 @@ var { } = require('ReactPriorityLevel'); var { findNextUnitOfWorkAtPriority } = require('ReactFiberPendingWork'); -module.exports = function(config : HostConfig) { +module.exports = function(config : HostConfig) { function reconcileChildren(current, workInProgress, nextChildren) { const priority = workInProgress.pendingWorkPriority; diff --git a/src/renderers/shared/fiber/ReactFiberCommitWork.js b/src/renderers/shared/fiber/ReactFiberCommitWork.js index 1f3e5e5a22..9f27e76ae9 100644 --- a/src/renderers/shared/fiber/ReactFiberCommitWork.js +++ b/src/renderers/shared/fiber/ReactFiberCommitWork.js @@ -13,6 +13,7 @@ 'use strict'; import type { Fiber } from 'ReactFiber'; +import type { FiberRoot } from 'ReactFiberRoot'; import type { HostConfig } from 'ReactFiberReconciler'; var ReactTypeOfWork = require('ReactTypeOfWork'); @@ -22,23 +23,31 @@ var { HostComponent, } = ReactTypeOfWork; -module.exports = function(config : HostConfig) { +module.exports = function(config : HostConfig) { + const updateContainer = config.updateContainer; const commitUpdate = config.commitUpdate; function commitWork(finishedWork : Fiber) : void { switch (finishedWork.tag) { - case ClassComponent: + case ClassComponent: { // TODO: Fire componentDidMount/componentDidUpdate, update refs return; - case HostContainer: + } + case HostContainer: { // TODO: Attach children to root container. + const children = finishedWork.output; + const root : FiberRoot = finishedWork.stateNode; + const containerInfo : C = root.containerInfo; + updateContainer(containerInfo, children); return; - case HostComponent: + } + case HostComponent: { if (finishedWork.stateNode == null || !finishedWork.alternate) { throw new Error('This should only be done during updates.'); } - const children = finishedWork.output; + const child = (finishedWork.child : ?Fiber); + const children = (child && !child.sibling) ? (child.output : ?Fiber | I) : child; const newProps = finishedWork.memoizedProps; // If we have an alternate, that means this is an update and we need to // schedule a side-effect to do the updates. @@ -47,6 +56,7 @@ module.exports = function(config : HostConfig) { const instance : I = finishedWork.stateNode; commitUpdate(instance, oldProps, newProps, children); return; + } default: throw new Error('This unit of work tag should not have side-effects.'); } diff --git a/src/renderers/shared/fiber/ReactFiberCompleteWork.js b/src/renderers/shared/fiber/ReactFiberCompleteWork.js index 6f4752ccc0..c91ecfa82c 100644 --- a/src/renderers/shared/fiber/ReactFiberCompleteWork.js +++ b/src/renderers/shared/fiber/ReactFiberCompleteWork.js @@ -17,6 +17,8 @@ import type { Fiber } from 'ReactFiber'; import type { HostConfig } from 'ReactFiberReconciler'; import type { ReifiedYield } from 'ReactReifiedYield'; +import type { TypeOfWork } from 'ReactTypeOfWork'; + var ReactChildFiber = require('ReactChildFiber'); var ReactTypeOfWork = require('ReactTypeOfWork'); var { @@ -30,7 +32,7 @@ var { YieldComponent, } = ReactTypeOfWork; -module.exports = function(config : HostConfig) { +module.exports = function(config : HostConfig) { const createInstance = config.createInstance; const prepareUpdate = config.prepareUpdate; @@ -120,12 +122,22 @@ module.exports = function(config : HostConfig) { transferOutput(workInProgress.child, workInProgress); return null; case HostContainer: + transferOutput(workInProgress.child, workInProgress); + // We don't know if a container has updated any children so we always + // need to update it right now. We schedule this side-effect after + // all the other side-effects in the subtree. + // TODO: I just realized that this means that nodes are not in the DOM + // document when componentDidMount/Update fires. I'll need to change + // the order for host component effects to happen *before* their + // children I think. + markForPostEffect(workInProgress); return null; case HostComponent: console.log('/host component', workInProgress.type); - transferOutput(workInProgress.child, workInProgress); - const children = workInProgress.output; - const newProps = workInProgress.memoizedProps; + const child = (workInProgress.child : ?Fiber); + const children = (child && !child.sibling) ? (child.output : ?Fiber | I) : child; + const newProps = workInProgress.pendingProps; + workInProgress.memoizedProps = newProps; if (workInProgress.alternate && workInProgress.stateNode != null) { // If we have an alternate, that means this is an update and we need to // schedule a side-effect to do the updates. @@ -136,9 +148,12 @@ module.exports = function(config : HostConfig) { // This returns true if there was something to update. markForPostEffect(workInProgress); } + workInProgress.output = instance; } else { const instance = createInstance(workInProgress.type, newProps, children); + // TODO: This seems like unnecessary duplication. workInProgress.stateNode = instance; + workInProgress.output = instance; } return null; case CoroutineComponent: diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index 28421314ab..cb22e88421 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -14,6 +14,7 @@ import type { Fiber } from 'ReactFiber'; import type { FiberRoot } from 'ReactFiberRoot'; +import type { TypeOfWork } from 'ReactTypeOfWork'; var { createFiberRoot } = require('ReactFiberRoot'); var ReactFiberScheduler = require('ReactFiberScheduler'); @@ -26,11 +27,17 @@ type Deadline = { timeRemaining : () => number }; -type HostChildNode = { output: HostChildren, sibling: ?HostChildNode }; +type HostChildNode = { tag: TypeOfWork, output: HostChildren, sibling: any }; export type HostChildren = null | void | I | HostChildNode; -export type HostConfig = { +export type HostConfig = { + + // TODO: We don't currently have a quick way to detect that children didn't + // reorder so we host will always need to check the set. We should make a flag + // or something so that it can bailout easily. + + updateContainer(containerInfo : C, children : HostChildren) : void; createInstance(type : T, props : P, children : HostChildren) : I, prepareUpdate(instance : I, oldProps : P, newProps : P, children : HostChildren) : bool, @@ -44,22 +51,22 @@ export type HostConfig = { type OpaqueNode = Fiber; -export type Reconciler = { - mountContainer(element : ReactElement, containerInfo : ?Object) : OpaqueNode, +export type Reconciler = { + mountContainer(element : ReactElement, containerInfo : C) : OpaqueNode, updateContainer(element : ReactElement, container : OpaqueNode) : void, unmountContainer(container : OpaqueNode) : void, // Used to extract the return value from the initial render. Legacy API. - getPublicRootInstance(container : OpaqueNode) : ?Object, + getPublicRootInstance(container : OpaqueNode) : (C | null), }; -module.exports = function(config : HostConfig) : Reconciler { +module.exports = function(config : HostConfig) : Reconciler { var { scheduleLowPriWork } = ReactFiberScheduler(config); return { - mountContainer(element : ReactElement, containerInfo : ?Object) : OpaqueNode { + mountContainer(element : ReactElement, containerInfo : C) : OpaqueNode { const root = createFiberRoot(containerInfo); const container = root.current; // TODO: Use pending work/state instead of props. @@ -94,7 +101,7 @@ module.exports = function(config : HostConfig) : Reconciler { scheduleLowPriWork(root); }, - getPublicRootInstance(container : OpaqueNode) : ?Object { + getPublicRootInstance(container : OpaqueNode) : (C | null) { return null; }, diff --git a/src/renderers/shared/fiber/ReactFiberRoot.js b/src/renderers/shared/fiber/ReactFiberRoot.js index ce186c3fce..c0b312253c 100644 --- a/src/renderers/shared/fiber/ReactFiberRoot.js +++ b/src/renderers/shared/fiber/ReactFiberRoot.js @@ -18,7 +18,7 @@ const { createHostContainerFiber } = require('ReactFiber'); export type FiberRoot = { // Any additional information from the host associated with this root. - containerInfo: ?Object, + containerInfo: 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. @@ -27,7 +27,7 @@ export type FiberRoot = { nextScheduledRoot: ?FiberRoot, }; -exports.createFiberRoot = function(containerInfo : ?Object) : FiberRoot { +exports.createFiberRoot = function(containerInfo : any) : FiberRoot { // Cyclic construction. This cheats the type system right now because // stateNode is any. const uninitializedFiber = createHostContainerFiber(); diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index c3aab11536..6df6198f9f 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -32,7 +32,7 @@ var { var timeHeuristicForUnitOfWork = 1; -module.exports = function(config : HostConfig) { +module.exports = function(config : HostConfig) { const { beginWork } = ReactFiberBeginWork(config); const { completeWork } = ReactFiberCompleteWork(config); diff --git a/src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js b/src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js new file mode 100644 index 0000000000..c59c12b97d --- /dev/null +++ b/src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js @@ -0,0 +1,97 @@ +/** + * 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'; + +var React; +var ReactNoop; + +describe('ReactIncremental', function() { + beforeEach(function() { + React = require('React'); + ReactNoop = require('ReactNoop'); + spyOn(console, 'log'); + }); + + function div(...children) { + return { type: 'div', children }; + } + + function span(...children) { + return { type: 'span', children }; + } + + it('can update child nodes of a host instance', function() { + + function Bar(props) { + return {props.text}; + } + + function Foo(props) { + return ( +
+ + {props.text === 'World' ? : null} +
+ ); + } + + ReactNoop.render(); + ReactNoop.flush(); + expect(ReactNoop.root.children).toEqual([ + div(span()), + ]); + + ReactNoop.render(); + ReactNoop.flush(); + expect(ReactNoop.root.children).toEqual([ + div(span(), span()), + ]); + + }); + + it('does not update child nodes if a flush is aborted', function() { + + function Bar(props) { + return {props.text}; + } + + function Foo(props) { + return ( +
+
+ + {props.text === 'Hello' ? : null} +
+ +
+ ); + } + + ReactNoop.render(); + ReactNoop.flush(); + expect(ReactNoop.root.children).toEqual([ + div(div(span(), span()), span()), + ]); + + ReactNoop.render(); + ReactNoop.flushLowPri(35); + expect(ReactNoop.root.children).toEqual([ + div(div(span(), span()), span()), + ]); + + }); + + // TODO: Test that side-effects are not cut off when a work in progress node + // moves to "current" without flushing due to having lower priority. Does this + // even happen? Maybe a child doesn't get processed because it is lower prio? + +});