Merge pull request #8083 from sebmarkbage/fiberfinddomnode

[Fiber] Implement findDOMNode and isMounted
This commit is contained in:
Sebastian Markbåge
2016-10-26 02:52:20 -04:00
committed by GitHub
9 changed files with 564 additions and 11 deletions
+12
View File
@@ -146,6 +146,18 @@ var ReactDOM = {
}
},
findDOMNode(componentOrElement : Element | ?ReactComponent<any, any, any>) : 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;
@@ -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(
<Text value="foo" ref={ref => 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 [
<div />,
<span />,
];
}
}
let instance = null;
ReactDOM.render(
<Fragment ref={ref => 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 [
<Wrapper><div /></Wrapper>,
<span />,
];
}
}
let instance = null;
ReactDOM.render(
<Fragment ref={ref => 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 [
<NullComponent />,
<div />,
<span />,
];
}
}
let instance = null;
ReactDOM.render(
<Fragment ref={ref => instance = ref} />,
container
);
expect(container.childNodes.length).toBe(2);
const firstNode = ReactDOM.findDOMNode(instance);
expect(firstNode).toBe(container.firstChild);
expect(firstNode.tagName).toBe('DIV');
});
}
});
+12
View File
@@ -154,6 +154,18 @@ var ReactNoop = {
}
},
findInstance(componentOrElement : Element | ?ReactComponent<any, any, any>) : 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) {
@@ -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 ?
@@ -182,15 +182,9 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
}
}
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<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
}
}
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: {
@@ -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<T, P, I, TI, C> = {
type OpaqueNode = Fiber;
export type Reconciler<C, I> = {
export type Reconciler<C, I, TI> = {
mountContainer(element : ReactElement<any>, containerInfo : C) : OpaqueNode,
updateContainer(element : ReactElement<any>, 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<any, any, any> | I | null),
getPublicRootInstance(container : OpaqueNode) : (ReactComponent<any, any, any> | TI | I | null),
// Use for findDOMNode/findHostNode. Legacy API.
findHostInstance(component : ReactComponent<any, any, any>) : I | TI | null,
};
module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) : Reconciler<C, I> {
module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) : Reconciler<C, I, TI> {
var { scheduleWork, performWithPriority } = ReactFiberScheduler(config);
@@ -122,7 +127,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) :
performWithPriority,
getPublicRootInstance(container : OpaqueNode) : (ReactComponent<any, any, any> | I | null) {
getPublicRootInstance(container : OpaqueNode) : (ReactComponent<any, any, any> | I | TI | null) {
const root : FiberRoot = (container.stateNode : any);
const containerFiber = root.current;
if (!containerFiber.child) {
@@ -131,6 +136,14 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) :
return containerFiber.child.stateNode;
},
findHostInstance(component : ReactComponent<any, any, any>) : I | TI | null {
const fiber = findCurrentHostFiber(component);
if (!fiber) {
return null;
}
return fiber.stateNode;
},
};
};
@@ -119,12 +119,18 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
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<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
// 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;
}
@@ -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<any, any, any>) : boolean {
var fiber : ?Fiber = ReactInstanceMap.get(component);
if (!fiber) {
return false;
}
return isFiberMounted(fiber);
};
exports.findCurrentHostFiber = function(component : ReactComponent<any, any, any>) : 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;
};
@@ -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 <span />;
},
});
function Foo() {
return <Component />;
}
ReactNoop.render(<Foo />);
// 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 <span />;
},
});
function Other() {
ops.push('Other');
return <span />;
}
function Foo(props) {
return props.mount ? <Component /> : <Other />;
}
ReactNoop.render(<Foo mount={true} />);
ReactNoop.flush();
expect(ops).toEqual(['Component']);
ops = [];
expect(instances[0].isMounted()).toBe(true);
ReactNoop.render(<Foo mount={false} />);
// 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 ?
<span ref={ref => this.span = ref} /> :
this.props.step === 2 ?
<div ref={ref => this.div = ref} /> :
null;
}
}
function Sibling() {
// Sibling is used to assert that we've rendered past the first component.
ops.push('render sibling');
return <span />;
}
function Foo(props) {
return [
<Component step={props.step} />,
<Sibling />,
];
}
ReactNoop.render(<Foo step={0} />);
// 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(<Foo step={1} />);
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(<Foo step={2} />);
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(<Foo step={3} />);
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);
});
});