From 298d0c3e05e618b7c46b7da0a4e214a20ead3670 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Sat, 22 Oct 2016 17:41:46 -0700 Subject: [PATCH 1/6] Implement findDOMNode for Fiber This is the naive implementation that doesn't cover the case where there are two diverging fibers, or if this tree is unmounted. --- src/renderers/dom/fiber/ReactDOMFiber.js | 12 +++++ .../shared/fiber/ReactFiberReconciler.js | 21 ++++++-- .../shared/fiber/ReactFiberTreeReflection.js | 54 +++++++++++++++++++ 3 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 src/renderers/shared/fiber/ReactFiberTreeReflection.js 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/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/ReactFiberTreeReflection.js b/src/renderers/shared/fiber/ReactFiberTreeReflection.js new file mode 100644 index 0000000000000..8fddb2fec13b3 --- /dev/null +++ b/src/renderers/shared/fiber/ReactFiberTreeReflection.js @@ -0,0 +1,54 @@ +/** + * 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 { + ClassComponent, + HostContainer, + HostComponent, + HostText, +} = require('ReactTypeOfWork'); + +exports.findCurrentHostFiber = function(component : ReactComponent) : Fiber | null { + var parent : ?Fiber = ReactInstanceMap.get(component); + if (!parent) { + return null; + } + + // TODO: This search is incomplete because this could be one of two possible fibers. + let node : Fiber = parent; + while (true) { + 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; +}; From 2e040fc2c1457deb60f9baabac9307621d4e6c1a Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Sat, 22 Oct 2016 18:35:07 -0700 Subject: [PATCH 2/6] Implement isMounted for Fiber This is the naive implementation that doesn't cover the case where it has completed but not yet committed. It also doesn't deal with unmounts since they currently don't clean up the item in the ReactInstanceMap. --- .../shared/fiber/ReactFiberClassComponent.js | 2 ++ .../shared/fiber/ReactFiberTreeReflection.js | 11 +++++++++++ 2 files changed, 13 insertions(+) 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/ReactFiberTreeReflection.js b/src/renderers/shared/fiber/ReactFiberTreeReflection.js index 8fddb2fec13b3..45e2bebb5b474 100644 --- a/src/renderers/shared/fiber/ReactFiberTreeReflection.js +++ b/src/renderers/shared/fiber/ReactFiberTreeReflection.js @@ -23,6 +23,17 @@ var { HostText, } = require('ReactTypeOfWork'); +exports.isMounted = function(component : ReactComponent) : boolean { + var parent : ?Fiber = ReactInstanceMap.get(component); + if (!parent) { + return false; + } + // TODO: This doesn't deal with the case where it has completed but not yet + // committed. It also doesn't deal with unmounts since they currently don't + // clean up the item in the ReactInstanceMap. + return true; +}; + exports.findCurrentHostFiber = function(component : ReactComponent) : Fiber | null { var parent : ?Fiber = ReactInstanceMap.get(component); if (!parent) { From 1376048bd9f4b87882304296faeec54acea5717a Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Mon, 24 Oct 2016 12:06:59 -0700 Subject: [PATCH 3/6] Clear effectTag and return pointers after side-effects This will let us use these pointers to reason about a tree. Whether if it is "current" or "work in progress". --- .../shared/fiber/ReactFiberCommitWork.js | 18 +++++++++++++++--- .../shared/fiber/ReactFiberScheduler.js | 3 +++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/renderers/shared/fiber/ReactFiberCommitWork.js b/src/renderers/shared/fiber/ReactFiberCommitWork.js index a972dc56230f9..a2411f8eb08a6 100644 --- a/src/renderers/shared/fiber/ReactFiberCommitWork.js +++ b/src/renderers/shared/fiber/ReactFiberCommitWork.js @@ -186,13 +186,14 @@ module.exports = function(config : HostConfig) { // Recursively delete all host nodes from the parent. // TODO: Error handling. const parent = getHostParent(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) { + outer: while (true) { if (node.tag === HostComponent || node.tag === HostText) { commitNestedUnmounts(node); // After all the children have unmounted, it is now safe to remove the @@ -209,16 +210,27 @@ module.exports = function(config : HostConfig) { } } if (node === current) { - return; + break outer; } while (!node.sibling) { if (!node.return || node.return === current) { - return; + break outer; } node = node.return; } node = node.sibling; } + // 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 { diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index e7acda55c5231..fb2f043e24dc0 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -156,6 +156,9 @@ 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.effectTag = NoWork; effectfulFiber = next; } From 1914f94041b24b23f4a9cde536b804ee062735cd Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Mon, 24 Oct 2016 13:46:37 -0700 Subject: [PATCH 4/6] Handle unmounted and not-yet-inserted subtrees in isMounted There are two cases where we have a Fiber that is not actually mounted. Either it is part of a tree that has not yet been inserted or it is part of a tree that was unmounted. For the insertion case, we can check the parents to see if there is any insertion effect pending along the parent path. For deletions, we can now check if any of the return pointers is null without actually being the root. --- .../shared/fiber/ReactFiberCommitWork.js | 24 ++-- .../shared/fiber/ReactFiberScheduler.js | 7 +- .../shared/fiber/ReactFiberTreeReflection.js | 34 ++++- .../ReactIncrementalReflection-test.js | 127 ++++++++++++++++++ 4 files changed, 176 insertions(+), 16 deletions(-) create mode 100644 src/renderers/shared/fiber/__tests__/ReactIncrementalReflection-test.js diff --git a/src/renderers/shared/fiber/ReactFiberCommitWork.js b/src/renderers/shared/fiber/ReactFiberCommitWork.js index a2411f8eb08a6..c12c95629d979 100644 --- a/src/renderers/shared/fiber/ReactFiberCommitWork.js +++ b/src/renderers/shared/fiber/ReactFiberCommitWork.js @@ -182,18 +182,11 @@ 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; - outer: while (true) { + while (true) { if (node.tag === HostComponent || node.tag === HostText) { commitNestedUnmounts(node); // After all the children have unmounted, it is now safe to remove the @@ -210,16 +203,25 @@ module.exports = function(config : HostConfig) { } } if (node === current) { - break outer; + return; } while (!node.sibling) { if (!node.return || node.return === current) { - break outer; + return; } node = node.return; } node = node.sibling; } + } + + 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 diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index fb2f043e24dc0..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: { @@ -158,7 +164,6 @@ module.exports = function(config : HostConfig) { 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.effectTag = NoWork; effectfulFiber = next; } diff --git a/src/renderers/shared/fiber/ReactFiberTreeReflection.js b/src/renderers/shared/fiber/ReactFiberTreeReflection.js index 45e2bebb5b474..836b6609d3ee0 100644 --- a/src/renderers/shared/fiber/ReactFiberTreeReflection.js +++ b/src/renderers/shared/fiber/ReactFiberTreeReflection.js @@ -23,15 +23,41 @@ var { HostText, } = require('ReactTypeOfWork'); +var { + NoEffect, + Placement, +} = require('ReactTypeOfSideEffect'); + exports.isMounted = function(component : ReactComponent) : boolean { var parent : ?Fiber = ReactInstanceMap.get(component); if (!parent) { return false; } - // TODO: This doesn't deal with the case where it has completed but not yet - // committed. It also doesn't deal with unmounts since they currently don't - // clean up the item in the ReactInstanceMap. - return true; + let node = parent; + if (!parent.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.findCurrentHostFiber = function(component : ReactComponent) : Fiber | 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..acec3df6f4d8f --- /dev/null +++ b/src/renderers/shared/fiber/__tests__/ReactIncrementalReflection-test.js @@ -0,0 +1,127 @@ +/** + * 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); + + }); + +}); From d5752aca0dc40f4ab3ec480940d045dc1fddf889 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Mon, 24 Oct 2016 16:02:22 -0700 Subject: [PATCH 5/6] findDOMNode when a component is not yet mounted or unmounted First we need to check if a component subtree is mounted at all. If it is, we need to search down the fiber for the first host node. However, we might be searching the "work in progress" instead of current. One realization is that it doesn't matter if we search work in progress or current if they're the same. They will generally be the same unless there is an insertion pending or something in the alternate tree was already deleted. So if we find one of those cases, we switch to look in the alternate tree instead. --- src/renderers/noop/ReactNoop.js | 12 ++ .../shared/fiber/ReactFiberTreeReflection.js | 45 ++++-- .../ReactIncrementalReflection-test.js | 148 ++++++++++++++++++ 3 files changed, 195 insertions(+), 10 deletions(-) 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/ReactFiberTreeReflection.js b/src/renderers/shared/fiber/ReactFiberTreeReflection.js index 836b6609d3ee0..d806992e74d02 100644 --- a/src/renderers/shared/fiber/ReactFiberTreeReflection.js +++ b/src/renderers/shared/fiber/ReactFiberTreeReflection.js @@ -17,7 +17,6 @@ import type { Fiber } from 'ReactFiber'; var ReactInstanceMap = require('ReactInstanceMap'); var { - ClassComponent, HostContainer, HostComponent, HostText, @@ -28,13 +27,9 @@ var { Placement, } = require('ReactTypeOfSideEffect'); -exports.isMounted = function(component : ReactComponent) : boolean { - var parent : ?Fiber = ReactInstanceMap.get(component); - if (!parent) { - return false; - } - let node = parent; - if (!parent.alternate) { +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) { @@ -58,17 +53,47 @@ exports.isMounted = function(component : ReactComponent) : boolea } // 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 { - var parent : ?Fiber = ReactInstanceMap.get(component); + let parent = ReactInstanceMap.get(component); if (!parent) { return null; } - // TODO: This search is incomplete because this could be one of two possible fibers. + 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) { diff --git a/src/renderers/shared/fiber/__tests__/ReactIncrementalReflection-test.js b/src/renderers/shared/fiber/__tests__/ReactIncrementalReflection-test.js index acec3df6f4d8f..6eb02db48c006 100644 --- a/src/renderers/shared/fiber/__tests__/ReactIncrementalReflection-test.js +++ b/src/renderers/shared/fiber/__tests__/ReactIncrementalReflection-test.js @@ -124,4 +124,152 @@ describe('ReactIncrementalReflection', () => { }); + 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); + }); + + }); From 8e2d7f8d2705159ca354294018bb98db1e0f4a71 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Mon, 24 Oct 2016 16:16:42 -0700 Subject: [PATCH 6/6] Add tests for findDOMNode on fragment and text These are new features that aren't covered by existing tests. It is now possible to use findDOMNode to find a text node. When a component returns a fragment, it will search to find the first host component just like element.querySelector does. --- .../dom/fiber/__tests__/ReactDOMFiber-test.js | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) 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'); + }); } });