diff --git a/src/renderers/dom/fiber/ReactDOMFiber.js b/src/renderers/dom/fiber/ReactDOMFiber.js index f1f7163bc63c9..9a5473bfd9e0d 100644 --- a/src/renderers/dom/fiber/ReactDOMFiber.js +++ b/src/renderers/dom/fiber/ReactDOMFiber.js @@ -146,6 +146,18 @@ var ReactDOM = { } }, + findDOMNode(componentOrElement : Element | ?ReactComponent) : null | Element | Text { + if (componentOrElement == null) { + return null; + } + // Unsound duck typing. + const component = (componentOrElement : any); + if (component.nodeType === 1) { + return component; + } + return DOMRenderer.findHostInstance(component); + }, + }; module.exports = ReactDOM; diff --git a/src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js b/src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js index 96f447505b229..f65bbb000eec0 100644 --- a/src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js +++ b/src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js @@ -64,5 +64,106 @@ describe('ReactDOMFiber', () => { expect(container.textContent).toEqual('10'); }); + + it('finds the DOM Text node of a string child', () => { + class Text extends React.Component { + render() { + return this.props.value; + } + } + + let instance = null; + ReactDOM.render( + instance = ref} />, + container + ); + + const textNode = ReactDOM.findDOMNode(instance); + expect(textNode).toBe(container.firstChild); + expect(textNode.nodeType).toBe(3); + expect(textNode.nodeValue).toBe('foo'); + }); + + it('finds the first child when a component returns a fragment', () => { + class Fragment extends React.Component { + render() { + return [ +
, + , + ]; + } + } + + let instance = null; + ReactDOM.render( + instance = ref} />, + container + ); + + expect(container.childNodes.length).toBe(2); + + const firstNode = ReactDOM.findDOMNode(instance); + expect(firstNode).toBe(container.firstChild); + expect(firstNode.tagName).toBe('DIV'); + }); + + it('finds the first child even when fragment is nested', () => { + class Wrapper extends React.Component { + render() { + return this.props.children; + } + } + + class Fragment extends React.Component { + render() { + return [ +
, + , + ]; + } + } + + let instance = null; + ReactDOM.render( + instance = ref} />, + container + ); + + expect(container.childNodes.length).toBe(2); + + const firstNode = ReactDOM.findDOMNode(instance); + expect(firstNode).toBe(container.firstChild); + expect(firstNode.tagName).toBe('DIV'); + }); + + it('finds the first child even when first child renders null', () => { + class NullComponent extends React.Component { + render() { + return null; + } + } + + class Fragment extends React.Component { + render() { + return [ + , +
, + , + ]; + } + } + + let instance = null; + ReactDOM.render( + instance = ref} />, + container + ); + + expect(container.childNodes.length).toBe(2); + + const firstNode = ReactDOM.findDOMNode(instance); + expect(firstNode).toBe(container.firstChild); + expect(firstNode.tagName).toBe('DIV'); + }); } }); diff --git a/src/renderers/noop/ReactNoop.js b/src/renderers/noop/ReactNoop.js index 0ac23ac0c3fa8..9b9c13673f850 100644 --- a/src/renderers/noop/ReactNoop.js +++ b/src/renderers/noop/ReactNoop.js @@ -154,6 +154,18 @@ var ReactNoop = { } }, + findInstance(componentOrElement : Element | ?ReactComponent) : null | Instance | TextInstance { + if (componentOrElement == null) { + return null; + } + // Unsound duck typing. + const component = (componentOrElement : any); + if (component.tag === TERMINAL_TAG || component.tag === TEXT_TAG) { + return component; + } + return NoopRenderer.findHostInstance(component); + }, + flushAnimationPri() { var cb = scheduledAnimationCallback; if (cb === null) { diff --git a/src/renderers/shared/fiber/ReactFiberClassComponent.js b/src/renderers/shared/fiber/ReactFiberClassComponent.js index 5e5c18f81eb30..4a8c27308b041 100644 --- a/src/renderers/shared/fiber/ReactFiberClassComponent.js +++ b/src/renderers/shared/fiber/ReactFiberClassComponent.js @@ -23,6 +23,7 @@ var { addCallbackToQueue, mergeUpdateQueue, } = require('ReactFiberUpdateQueue'); +var { isMounted } = require('ReactFiberTreeReflection'); var ReactInstanceMap = require('ReactInstanceMap'); module.exports = function(scheduleUpdate : (fiber: Fiber, priorityLevel : PriorityLevel) => void) { @@ -39,6 +40,7 @@ module.exports = function(scheduleUpdate : (fiber: Fiber, priorityLevel : Priori // Class component state updater const updater = { + isMounted, enqueueSetState(instance, partialState) { const fiber = ReactInstanceMap.get(instance); const updateQueue = fiber.updateQueue ? diff --git a/src/renderers/shared/fiber/ReactFiberCommitWork.js b/src/renderers/shared/fiber/ReactFiberCommitWork.js index a972dc56230f9..c12c95629d979 100644 --- a/src/renderers/shared/fiber/ReactFiberCommitWork.js +++ b/src/renderers/shared/fiber/ReactFiberCommitWork.js @@ -182,15 +182,9 @@ module.exports = function(config : HostConfig) { } } - function commitDeletion(current : Fiber) : void { - // Recursively delete all host nodes from the parent. - // TODO: Error handling. - const parent = getHostParent(current); + function unmountHostComponents(parent, current) { // We only have the top Fiber that was inserted but we need recurse down its // children to find all the terminal nodes. - // TODO: Call componentWillUnmount on all classes as needed. Recurse down - // removed HostComponents but don't call removeChild on already removed - // children. let node : Fiber = current; while (true) { if (node.tag === HostComponent || node.tag === HostText) { @@ -221,6 +215,26 @@ module.exports = function(config : HostConfig) { } } + function commitDeletion(current : Fiber) : void { + // Recursively delete all host nodes from the parent. + // TODO: Error handling. + const parent = getHostParent(current); + + unmountHostComponents(parent, current); + + // Cut off the return pointers to disconnect it from the tree. Ideally, we + // should clear the child pointer of the parent alternate to let this + // get GC:ed but we don't know which for sure which parent is the current + // one so we'll settle for GC:ing the subtree of this child. This child + // itself will be GC:ed when the parent updates the next time. + current.return = null; + current.child = null; + if (current.alternate) { + current.alternate.child = null; + current.alternate.return = null; + } + } + function commitUnmount(current : Fiber) : void { switch (current.tag) { case ClassComponent: { diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index be3b57fbb1c5d..f775e6b1f0884 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -24,6 +24,8 @@ if (__DEV__) { var ReactFiberInstrumentation = require('ReactFiberInstrumentation'); } +var { findCurrentHostFiber } = require('ReactFiberTreeReflection'); + type Deadline = { timeRemaining : () => number }; @@ -58,17 +60,20 @@ export type HostConfig = { type OpaqueNode = Fiber; -export type Reconciler = { +export type Reconciler = { mountContainer(element : ReactElement, containerInfo : C) : OpaqueNode, updateContainer(element : ReactElement, container : OpaqueNode) : void, unmountContainer(container : OpaqueNode) : void, performWithPriority(priorityLevel : PriorityLevel, fn : Function) : void, // Used to extract the return value from the initial render. Legacy API. - getPublicRootInstance(container : OpaqueNode) : (ReactComponent | I | null), + getPublicRootInstance(container : OpaqueNode) : (ReactComponent | TI | I | null), + + // Use for findDOMNode/findHostNode. Legacy API. + findHostInstance(component : ReactComponent) : I | TI | null, }; -module.exports = function(config : HostConfig) : Reconciler { +module.exports = function(config : HostConfig) : Reconciler { var { scheduleWork, performWithPriority } = ReactFiberScheduler(config); @@ -122,7 +127,7 @@ module.exports = function(config : HostConfig) : performWithPriority, - getPublicRootInstance(container : OpaqueNode) : (ReactComponent | I | null) { + getPublicRootInstance(container : OpaqueNode) : (ReactComponent | I | TI | null) { const root : FiberRoot = (container.stateNode : any); const containerFiber = root.current; if (!containerFiber.child) { @@ -131,6 +136,14 @@ module.exports = function(config : HostConfig) : return containerFiber.child.stateNode; }, + findHostInstance(component : ReactComponent) : I | TI | null { + const fiber = findCurrentHostFiber(component); + if (!fiber) { + return null; + } + return fiber.stateNode; + }, + }; }; diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index e7acda55c5231..5677ffafcb357 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -119,12 +119,18 @@ module.exports = function(config : HostConfig) { switch (effectfulFiber.effectTag) { case Placement: { commitInsertion(effectfulFiber); + // Clear the effect tag so that we know that this is inserted, before + // any life-cycles like componentDidMount gets called. + effectfulFiber.effectTag = NoWork; break; } case PlacementAndUpdate: { commitInsertion(effectfulFiber); const current = effectfulFiber.alternate; commitWork(current, effectfulFiber); + // Clear the effect tag so that we know that this is inserted, before + // any life-cycles like componentDidMount gets called. + effectfulFiber.effectTag = Update; break; } case Update: { @@ -156,6 +162,8 @@ module.exports = function(config : HostConfig) { // and lastEffect since they're on every node, not just the effectful // ones. So we have to clean everything as we reuse nodes anyway. effectfulFiber.nextEffect = null; + // Ensure that we reset the effectTag here so that we can rely on effect + // tags to reason about the current life-cycle. effectfulFiber = next; } diff --git a/src/renderers/shared/fiber/ReactFiberTreeReflection.js b/src/renderers/shared/fiber/ReactFiberTreeReflection.js new file mode 100644 index 0000000000000..d806992e74d02 --- /dev/null +++ b/src/renderers/shared/fiber/ReactFiberTreeReflection.js @@ -0,0 +1,116 @@ +/** + * 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. + * + * @providesModule ReactFiberTreeReflection + * @flow + */ + +'use strict'; + +import type { Fiber } from 'ReactFiber'; + +var ReactInstanceMap = require('ReactInstanceMap'); + +var { + HostContainer, + HostComponent, + HostText, +} = require('ReactTypeOfWork'); + +var { + NoEffect, + Placement, +} = require('ReactTypeOfSideEffect'); + +function isFiberMounted(fiber : Fiber) : boolean { + let node = fiber; + if (!fiber.alternate) { + // If there is no alternate, this might be a new tree that isn't inserted + // yet. If it is, then it will have a pending insertion effect on it. + if ((node.effectTag & Placement) !== NoEffect) { + return false; + } + while (node.return) { + node = node.return; + if ((node.effectTag & Placement) !== NoEffect) { + return false; + } + } + } else { + while (node.return) { + node = node.return; + } + } + if (node.tag === HostContainer) { + // TODO: Check if this was a nested HostContainer when used with + // renderContainerIntoSubtree. + return true; + } + // If we didn't hit the root, that means that we're in an disconnected tree. + return false; +} + +exports.isMounted = function(component : ReactComponent) : boolean { + var fiber : ?Fiber = ReactInstanceMap.get(component); + if (!fiber) { + return false; + } + return isFiberMounted(fiber); +}; + +exports.findCurrentHostFiber = function(component : ReactComponent) : Fiber | null { + let parent = ReactInstanceMap.get(component); + if (!parent) { + return null; + } + + if (!isFiberMounted(parent)) { + // First check if this node itself is mounted. + return null; + } + + let didTryOtherTree = false; + + // Next we'll drill down this component to find the first HostComponent/Text. + let node : Fiber = parent; + while (true) { + if ((node.effectTag & Placement) !== NoEffect || !node.return) { + // If any node along the way was deleted, or is an insertion, that means + // that we're actually in a work in progress to update this component with + // a different component. We need to look in the "current" fiber instead. + if (!parent.alternate) { + return null; + } + if (didTryOtherTree) { + // Safety, to avoid an infinite loop if something goes wrong. + throw new Error('This should never hit this infinite loop.'); + } + didTryOtherTree = true; + node = parent = parent.alternate; + continue; + } + if (node.tag === HostComponent || node.tag === HostText) { + return node; + } else if (node.child) { + // TODO: Coroutines need to visit the stateNode. + node = node.child; + continue; + } + if (node === parent) { + return null; + } + while (!node.sibling) { + if (!node.return || node.return === parent) { + return null; + } + node = node.return; + } + node = node.sibling; + } + return null; +}; diff --git a/src/renderers/shared/fiber/__tests__/ReactIncrementalReflection-test.js b/src/renderers/shared/fiber/__tests__/ReactIncrementalReflection-test.js new file mode 100644 index 0000000000000..6eb02db48c006 --- /dev/null +++ b/src/renderers/shared/fiber/__tests__/ReactIncrementalReflection-test.js @@ -0,0 +1,275 @@ +/** + * 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('ReactIncrementalReflection', () => { + beforeEach(() => { + React = require('React'); + ReactNoop = require('ReactNoop'); + }); + + it('handles isMounted even when the initial render is deferred', () => { + + let ops = []; + + const instances = []; + + const Component = React.createClass({ + componentWillMount() { + instances.push(this); + ops.push('componentWillMount', this.isMounted()); + }, + componentDidMount() { + ops.push('componentDidMount', this.isMounted()); + }, + render() { + return ; + }, + }); + + function Foo() { + return ; + } + + ReactNoop.render(); + + // Render part way through but don't yet commit the updates. + ReactNoop.flushDeferredPri(20); + + expect(ops).toEqual([ + 'componentWillMount', false, + ]); + + expect(instances[0].isMounted()).toBe(false); + + ops = []; + + // Render the rest and commit the updates. + ReactNoop.flush(); + + expect(ops).toEqual([ + 'componentDidMount', true, + ]); + + expect(instances[0].isMounted()).toBe(true); + + }); + + it('handles isMounted when an unmount is deferred', () => { + + let ops = []; + + const instances = []; + + const Component = React.createClass({ + componentWillMount() { + instances.push(this); + }, + componentWillUnmount() { + ops.push('componentWillUnmount', this.isMounted()); + }, + render() { + ops.push('Component'); + return ; + }, + }); + + function Other() { + ops.push('Other'); + return ; + } + + function Foo(props) { + return props.mount ? : ; + } + + ReactNoop.render(); + ReactNoop.flush(); + + expect(ops).toEqual(['Component']); + ops = []; + + expect(instances[0].isMounted()).toBe(true); + + ReactNoop.render(); + // Render part way through but don't yet commit the updates so it is not + // fully unmounted yet. + ReactNoop.flushDeferredPri(20); + + expect(ops).toEqual(['Other']); + ops = []; + + expect(instances[0].isMounted()).toBe(true); + + // Finish flushing the unmount. + ReactNoop.flush(); + + expect(ops).toEqual([ + 'componentWillUnmount', true, + ]); + + expect(instances[0].isMounted()).toBe(false); + + }); + + it('finds no node before insertion and correct node before deletion', () => { + + let ops = []; + + let classInstance = null; + + class Component extends React.Component { + componentWillMount() { + classInstance = this; + ops.push('componentWillMount', ReactNoop.findInstance(this)); + } + componentDidMount() { + ops.push('componentDidMount', ReactNoop.findInstance(this)); + } + componentWillUpdate() { + ops.push('componentWillUpdate', ReactNoop.findInstance(this)); + } + componentDidUpdate() { + ops.push('componentDidUpdate', ReactNoop.findInstance(this)); + } + render() { + ops.push('render'); + return this.props.step < 2 ? + this.span = ref} /> : + this.props.step === 2 ? +
this.div = ref} /> : + null; + } + } + + function Sibling() { + // Sibling is used to assert that we've rendered past the first component. + ops.push('render sibling'); + return ; + } + + function Foo(props) { + return [ + , + , + ]; + } + + ReactNoop.render(); + // Flush past Component but don't complete rendering everything yet. + ReactNoop.flushDeferredPri(30); + + expect(ops).toEqual([ + 'componentWillMount', null, + 'render', + 'render sibling', + ]); + + ops = []; + + expect(classInstance).toBeDefined(); + // The instance has been complete but is still not committed so it should + // not find any host nodes in it. + expect(ReactNoop.findInstance(classInstance)).toBe(null); + + ReactNoop.flush(); + + const hostSpan = classInstance.span; + expect(hostSpan).toBeDefined(); + + expect(ReactNoop.findInstance(classInstance)).toBe(hostSpan); + + expect(ops).toEqual([ + 'componentDidMount', hostSpan, + ]); + + ops = []; + + // Flush next step which will cause an update but not yet render a new host + // node. + ReactNoop.render(); + ReactNoop.flush(); + + expect(ops).toEqual([ + 'componentWillUpdate', hostSpan, + 'render', + 'render sibling', + 'componentDidUpdate', hostSpan, + ]); + + expect(ReactNoop.findInstance(classInstance)).toBe(hostSpan); + + ops = []; + + // The next step will render a new host node but won't get committed yet. + // We expect this to mutate the original Fiber. + ReactNoop.render(); + ReactNoop.flushDeferredPri(30); + + expect(ops).toEqual([ + 'componentWillUpdate', hostSpan, + 'render', + 'render sibling', + ]); + + ops = []; + + // This should still be the host span. + expect(ReactNoop.findInstance(classInstance)).toBe(hostSpan); + + // When we finally flush the tree it will get committed. + ReactNoop.flush(); + + const hostDiv = classInstance.div; + + expect(hostDiv).toBeDefined(); + expect(hostSpan).not.toBe(hostDiv); + + expect(ops).toEqual([ + 'componentDidUpdate', hostDiv, + ]); + + ops = []; + + // We should now find the new host node. + expect(ReactNoop.findInstance(classInstance)).toBe(hostDiv); + + // Finally we will render to null but not yet commit it. + ReactNoop.render(); + ReactNoop.flushDeferredPri(25); + + expect(ops).toEqual([ + 'componentWillUpdate', hostDiv, + 'render', + 'render sibling', + ]); + + ops = []; + + // This should still be the host div since the deletion is not committed. + expect(ReactNoop.findInstance(classInstance)).toBe(hostDiv); + + ReactNoop.flush(); + + expect(ops).toEqual([ + 'componentDidUpdate', null, + ]); + + // This should still be the host div since the deletion is not committed. + expect(ReactNoop.findInstance(classInstance)).toBe(null); + }); + + +});