diff --git a/src/isomorphic/ReactDebugInstanceMap.js b/src/isomorphic/ReactDebugInstanceMap.js deleted file mode 100644 index 50dddf4ad0e82..0000000000000 --- a/src/isomorphic/ReactDebugInstanceMap.js +++ /dev/null @@ -1,124 +0,0 @@ -/** - * Copyright 2016-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 ReactDebugInstanceMap - */ - -'use strict'; - -var warning = require('warning'); - -function checkValidInstance(internalInstance) { - if (!internalInstance) { - warning( - false, - 'There is an internal error in the React developer tools integration. ' + - 'Instead of an internal instance, received %s. ' + - 'Please report this as a bug in React.', - internalInstance - ); - return false; - } - var isValid = typeof internalInstance.mountComponent === 'function'; - warning( - isValid, - 'There is an internal error in the React developer tools integration. ' + - 'Instead of an internal instance, received an object with the following ' + - 'keys: %s. Please report this as a bug in React.', - Object.keys(internalInstance).join(', ') - ); - return isValid; -} - -var idCounter = 1; -var instancesByIDs = {}; -var instancesToIDs; - -function getIDForInstance(internalInstance) { - if (!instancesToIDs) { - instancesToIDs = new WeakMap(); - } - if (instancesToIDs.has(internalInstance)) { - return instancesToIDs.get(internalInstance); - } else { - var instanceID = (idCounter++).toString(); - instancesToIDs.set(internalInstance, instanceID); - return instanceID; - } -} - -function getInstanceByID(instanceID) { - return instancesByIDs[instanceID] || null; -} - -function isRegisteredInstance(internalInstance) { - var instanceID = getIDForInstance(internalInstance); - if (instanceID) { - return instancesByIDs.hasOwnProperty(instanceID); - } else { - return false; - } -} - -function registerInstance(internalInstance) { - var instanceID = getIDForInstance(internalInstance); - if (instanceID) { - instancesByIDs[instanceID] = internalInstance; - } -} - -function unregisterInstance(internalInstance) { - var instanceID = getIDForInstance(internalInstance); - if (instanceID) { - delete instancesByIDs[instanceID]; - } -} - -var ReactDebugInstanceMap = { - getIDForInstance(internalInstance) { - if (!checkValidInstance(internalInstance)) { - return null; - } - return getIDForInstance(internalInstance); - }, - getInstanceByID(instanceID) { - return getInstanceByID(instanceID); - }, - isRegisteredInstance(internalInstance) { - if (!checkValidInstance(internalInstance)) { - return false; - } - return isRegisteredInstance(internalInstance); - }, - registerInstance(internalInstance) { - if (!checkValidInstance(internalInstance)) { - return; - } - warning( - !isRegisteredInstance(internalInstance), - 'There is an internal error in the React developer tools integration. ' + - 'A registered instance should not be registered again. ' + - 'Please report this as a bug in React.' - ); - registerInstance(internalInstance); - }, - unregisterInstance(internalInstance) { - if (!checkValidInstance(internalInstance)) { - return; - } - warning( - isRegisteredInstance(internalInstance), - 'There is an internal error in the React developer tools integration. ' + - 'An unregistered instance should not be unregistered again. ' + - 'Please report this as a bug in React.' - ); - unregisterInstance(internalInstance); - }, -}; - -module.exports = ReactDebugInstanceMap; diff --git a/src/isomorphic/ReactDebugTool.js b/src/isomorphic/ReactDebugTool.js index 24f70dd758449..deb3ba73a4ae5 100644 --- a/src/isomorphic/ReactDebugTool.js +++ b/src/isomorphic/ReactDebugTool.js @@ -58,17 +58,29 @@ var ReactDebugTool = { onSetState() { emitEvent('onSetState'); }, - onMountRootComponent(internalInstance) { - emitEvent('onMountRootComponent', internalInstance); + onSetDisplayName(debugID, displayName) { + emitEvent('onSetDisplayName', debugID, displayName); }, - onMountComponent(internalInstance) { - emitEvent('onMountComponent', internalInstance); + onSetChildren(debugID, childDebugIDs) { + emitEvent('onSetChildren', debugID, childDebugIDs); }, - onUpdateComponent(internalInstance) { - emitEvent('onUpdateComponent', internalInstance); + onSetOwner(debugID, ownerDebugID) { + emitEvent('onSetOwner', debugID, ownerDebugID); }, - onUnmountComponent(internalInstance) { - emitEvent('onUnmountComponent', internalInstance); + onSetText(debugID, text) { + emitEvent('onSetText', debugID, text); + }, + onMountRootComponent(debugID) { + emitEvent('onMountRootComponent', debugID); + }, + onMountComponent(debugID) { + emitEvent('onMountComponent', debugID); + }, + onUpdateComponent(debugID) { + emitEvent('onUpdateComponent', debugID); + }, + onUnmountComponent(debugID) { + emitEvent('onUnmountComponent', debugID); }, onTestEvent() { emitEvent('onTestEvent'); diff --git a/src/isomorphic/__tests__/ReactDebugInstanceMap-test.js b/src/isomorphic/__tests__/ReactDebugInstanceMap-test.js deleted file mode 100644 index d9a063e2c649f..0000000000000 --- a/src/isomorphic/__tests__/ReactDebugInstanceMap-test.js +++ /dev/null @@ -1,173 +0,0 @@ -/** - * Copyright 2016-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'; - -describe('ReactDebugInstanceMap', function() { - var React; - var ReactDebugInstanceMap; - var ReactDOM; - - beforeEach(function() { - jest.resetModuleRegistry(); - React = require('React'); - ReactDebugInstanceMap = require('ReactDebugInstanceMap'); - ReactDOM = require('ReactDOM'); - }); - - function createStubInstance() { - return { mountComponent: () => {} }; - } - - it('should register and unregister instances', function() { - var inst1 = createStubInstance(); - var inst2 = createStubInstance(); - - expect(ReactDebugInstanceMap.isRegisteredInstance(inst1)).toBe(false); - expect(ReactDebugInstanceMap.isRegisteredInstance(inst2)).toBe(false); - - ReactDebugInstanceMap.registerInstance(inst1); - expect(ReactDebugInstanceMap.isRegisteredInstance(inst1)).toBe(true); - expect(ReactDebugInstanceMap.isRegisteredInstance(inst2)).toBe(false); - - ReactDebugInstanceMap.registerInstance(inst2); - expect(ReactDebugInstanceMap.isRegisteredInstance(inst1)).toBe(true); - expect(ReactDebugInstanceMap.isRegisteredInstance(inst2)).toBe(true); - - ReactDebugInstanceMap.unregisterInstance(inst2); - expect(ReactDebugInstanceMap.isRegisteredInstance(inst1)).toBe(true); - expect(ReactDebugInstanceMap.isRegisteredInstance(inst2)).toBe(false); - - ReactDebugInstanceMap.unregisterInstance(inst1); - expect(ReactDebugInstanceMap.isRegisteredInstance(inst1)).toBe(false); - expect(ReactDebugInstanceMap.isRegisteredInstance(inst2)).toBe(false); - }); - - it('should assign stable IDs', function() { - var inst1 = createStubInstance(); - var inst2 = createStubInstance(); - - var inst1ID = ReactDebugInstanceMap.getIDForInstance(inst1); - var inst2ID = ReactDebugInstanceMap.getIDForInstance(inst2); - expect(typeof inst1ID).toBe('string'); - expect(typeof inst2ID).toBe('string'); - expect(inst1ID).not.toBe(inst2ID); - - ReactDebugInstanceMap.registerInstance(inst1); - ReactDebugInstanceMap.registerInstance(inst2); - expect(ReactDebugInstanceMap.getIDForInstance(inst1)).toBe(inst1ID); - expect(ReactDebugInstanceMap.getIDForInstance(inst2)).toBe(inst2ID); - - ReactDebugInstanceMap.unregisterInstance(inst1); - ReactDebugInstanceMap.unregisterInstance(inst2); - expect(ReactDebugInstanceMap.getIDForInstance(inst1)).toBe(inst1ID); - expect(ReactDebugInstanceMap.getIDForInstance(inst2)).toBe(inst2ID); - }); - - it('should retrieve registered instance by its ID', function() { - var inst1 = createStubInstance(); - var inst2 = createStubInstance(); - - var inst1ID = ReactDebugInstanceMap.getIDForInstance(inst1); - var inst2ID = ReactDebugInstanceMap.getIDForInstance(inst2); - expect(ReactDebugInstanceMap.getInstanceByID(inst1ID)).toBe(null); - expect(ReactDebugInstanceMap.getInstanceByID(inst2ID)).toBe(null); - - ReactDebugInstanceMap.registerInstance(inst1); - ReactDebugInstanceMap.registerInstance(inst2); - expect(ReactDebugInstanceMap.getInstanceByID(inst1ID)).toBe(inst1); - expect(ReactDebugInstanceMap.getInstanceByID(inst2ID)).toBe(inst2); - - ReactDebugInstanceMap.unregisterInstance(inst1); - ReactDebugInstanceMap.unregisterInstance(inst2); - expect(ReactDebugInstanceMap.getInstanceByID(inst1ID)).toBe(null); - expect(ReactDebugInstanceMap.getInstanceByID(inst2ID)).toBe(null); - }); - - it('should warn when registering an instance twice', function() { - spyOn(console, 'error'); - - var inst = createStubInstance(); - ReactDebugInstanceMap.registerInstance(inst); - expect(console.error.argsForCall.length).toBe(0); - - ReactDebugInstanceMap.registerInstance(inst); - expect(console.error.argsForCall.length).toBe(1); - expect(console.error.argsForCall[0][0]).toContain( - 'There is an internal error in the React developer tools integration. ' + - 'A registered instance should not be registered again. ' + - 'Please report this as a bug in React.' - ); - - ReactDebugInstanceMap.unregisterInstance(inst); - ReactDebugInstanceMap.registerInstance(inst); - expect(console.error.argsForCall.length).toBe(1); - }); - - it('should warn when unregistering an instance twice', function() { - spyOn(console, 'error'); - var inst = createStubInstance(); - - ReactDebugInstanceMap.unregisterInstance(inst); - expect(console.error.argsForCall.length).toBe(1); - expect(console.error.argsForCall[0][0]).toContain( - 'There is an internal error in the React developer tools integration. ' + - 'An unregistered instance should not be unregistered again. ' + - 'Please report this as a bug in React.' - ); - - ReactDebugInstanceMap.registerInstance(inst); - ReactDebugInstanceMap.unregisterInstance(inst); - expect(console.error.argsForCall.length).toBe(1); - - ReactDebugInstanceMap.unregisterInstance(inst); - expect(console.error.argsForCall.length).toBe(2); - expect(console.error.argsForCall[1][0]).toContain( - 'There is an internal error in the React developer tools integration. ' + - 'An unregistered instance should not be unregistered again. ' + - 'Please report this as a bug in React.' - ); - }); - - it('should warn about anything than is not an internal instance', function() { - class Foo extends React.Component { - render() { - return
; - } - } - - spyOn(console, 'error'); - var warningCount = 0; - var div = document.createElement('div'); - var publicInst = ReactDOM.render(, div); - - [false, null, undefined, {}, div, publicInst].forEach(falsyValue => { - ReactDebugInstanceMap.registerInstance(falsyValue); - warningCount++; - expect(ReactDebugInstanceMap.getIDForInstance(falsyValue)).toBe(null); - warningCount++; - expect(ReactDebugInstanceMap.isRegisteredInstance(falsyValue)).toBe(false); - warningCount++; - ReactDebugInstanceMap.unregisterInstance(falsyValue); - warningCount++; - }); - - expect(console.error.argsForCall.length).toBe(warningCount); - for (var i = 0; i < warningCount.length; i++) { - // Ideally we could check for the more detailed error message here - // but it depends on the input type and is meant for internal bugs - // anyway so I don't think it's worth complicating the test with it. - expect(console.error.argsForCall[i][0]).toContain( - 'There is an internal error in the React developer tools integration.' - ); - } - }); -}); diff --git a/src/isomorphic/devtools/ReactComponentTreeDevtool.js b/src/isomorphic/devtools/ReactComponentTreeDevtool.js new file mode 100644 index 0000000000000..bdba5b378f31e --- /dev/null +++ b/src/isomorphic/devtools/ReactComponentTreeDevtool.js @@ -0,0 +1,148 @@ +/** + * Copyright 2016-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 ReactComponentTreeDevtool + */ + +'use strict'; + +var invariant = require('invariant'); + +var tree = {}; +var rootIDs = []; + +function updateTree(id, update) { + if (!tree[id]) { + tree[id] = { + parentID: null, + ownerID: null, + text: null, + childIDs: [], + displayName: 'Unknown', + isMounted: false, + }; + } + update(tree[id]); +} + +function purgeDeep(id) { + var item = tree[id]; + if (item) { + var {childIDs} = item; + delete tree[id]; + childIDs.forEach(purgeDeep); + } +} + +var ReactComponentTreeDevtool = { + onSetDisplayName(id, displayName) { + updateTree(id, item => item.displayName = displayName); + }, + + onSetChildren(id, nextChildIDs) { + updateTree(id, item => { + var prevChildIDs = item.childIDs; + item.childIDs = nextChildIDs; + + nextChildIDs.forEach(nextChildID => { + var nextChild = tree[nextChildID]; + invariant( + nextChild, + 'Expected devtool events to fire for the child ' + + 'before its parent includes it in onSetChildren().' + ); + invariant( + nextChild.displayName != null, + 'Expected onSetDisplayName() to fire for the child ' + + 'before its parent includes it in onSetChildren().' + ); + invariant( + nextChild.childIDs != null || nextChild.text != null, + 'Expected onSetChildren() or onSetText() to fire for the child ' + + 'before its parent includes it in onSetChildren().' + ); + invariant( + nextChild.isMounted, + 'Expected onMountComponent() to fire for the child ' + + 'before its parent includes it in onSetChildren().' + ); + + if (prevChildIDs.indexOf(nextChildID) === -1) { + nextChild.parentID = id; + } + }); + }); + }, + + onSetOwner(id, ownerID) { + updateTree(id, item => item.ownerID = ownerID); + }, + + onSetText(id, text) { + updateTree(id, item => item.text = text); + }, + + onMountComponent(id) { + updateTree(id, item => item.isMounted = true); + }, + + onMountRootComponent(id) { + rootIDs.push(id); + }, + + onUnmountComponent(id) { + updateTree(id, item => item.isMounted = false); + rootIDs = rootIDs.filter(rootID => rootID !== id); + }, + + purgeUnmountedComponents() { + Object.keys(tree) + .filter(id => !tree[id].isMounted) + .forEach(purgeDeep); + }, + + isMounted(id) { + var item = tree[id]; + return item ? item.isMounted : false; + }, + + getChildIDs(id) { + var item = tree[id]; + return item ? item.childIDs : []; + }, + + getDisplayName(id) { + var item = tree[id]; + return item ? item.displayName : 'Unknown'; + }, + + getOwnerID(id) { + var item = tree[id]; + return item ? item.ownerID : null; + }, + + getParentID(id) { + var item = tree[id]; + return item ? item.parentID : null; + }, + + getText(id) { + var item = tree[id]; + return item ? item.text : null; + }, + + getRootIDs() { + return rootIDs; + }, + + getRegisteredIDs() { + return Object.keys(tree); + }, +}; + +module.exports = ReactComponentTreeDevtool; diff --git a/src/isomorphic/devtools/__tests__/ReactComponentTreeDevtool-test.js b/src/isomorphic/devtools/__tests__/ReactComponentTreeDevtool-test.js new file mode 100644 index 0000000000000..48f3b9d3d9f3e --- /dev/null +++ b/src/isomorphic/devtools/__tests__/ReactComponentTreeDevtool-test.js @@ -0,0 +1,1725 @@ +/** + * Copyright 2016-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'; + +describe('ReactComponentTreeDevtool', () => { + var React; + var ReactDebugTool; + var ReactDOM; + var ReactDOMServer; + var ReactInstanceMap; + var ReactComponentTreeDevtool; + + beforeEach(() => { + jest.resetModuleRegistry(); + + React = require('React'); + ReactDebugTool = require('ReactDebugTool'); + ReactDOM = require('ReactDOM'); + ReactDOMServer = require('ReactDOMServer'); + ReactInstanceMap = require('ReactInstanceMap'); + ReactComponentTreeDevtool = require('ReactComponentTreeDevtool'); + + ReactDebugTool.addDevtool(ReactComponentTreeDevtool); + }); + + afterEach(() => { + ReactDebugTool.removeDevtool(ReactComponentTreeDevtool); + }); + + function getRootDisplayNames() { + return ReactComponentTreeDevtool.getRootIDs() + .map(ReactComponentTreeDevtool.getDisplayName); + } + + function getRegisteredDisplayNames() { + return ReactComponentTreeDevtool.getRegisteredIDs() + .map(ReactComponentTreeDevtool.getDisplayName); + } + + function getTree(rootID, options = {}) { + var { + includeOwnerDisplayName = false, + includeParentDisplayName = false, + expectedParentID = null, + } = options; + + var result = { + displayName: ReactComponentTreeDevtool.getDisplayName(rootID), + }; + + var ownerID = ReactComponentTreeDevtool.getOwnerID(rootID); + var parentID = ReactComponentTreeDevtool.getParentID(rootID); + expect(parentID).toBe(expectedParentID); + + if (includeParentDisplayName && parentID) { + result.parentDisplayName = ReactComponentTreeDevtool.getDisplayName(parentID); + } + if (includeOwnerDisplayName && ownerID) { + result.ownerDisplayName = ReactComponentTreeDevtool.getDisplayName(ownerID); + } + + var childIDs = ReactComponentTreeDevtool.getChildIDs(rootID); + var text = ReactComponentTreeDevtool.getText(rootID); + if (text != null) { + result.text = text; + } else { + result.children = childIDs.map(childID => + getTree(childID, {...options, expectedParentID: rootID }) + ); + } + + return result; + } + + function assertTreeMatches(pairs, options) { + if (!Array.isArray(pairs[0])) { + pairs = [pairs]; + } + + var node = document.createElement('div'); + var currentElement; + var rootInstance; + + class Wrapper extends React.Component { + render() { + rootInstance = ReactInstanceMap.get(this); + return currentElement; + } + } + + function getActualTree() { + return getTree(rootInstance._debugID, options).children[0]; + } + + pairs.forEach(([element, expectedTree]) => { + currentElement = element; + ReactDOM.render(, node); + expect(getActualTree()).toEqual(expectedTree); + ReactComponentTreeDevtool.purgeUnmountedComponents(); + expect(getActualTree()).toEqual(expectedTree); + }); + ReactDOM.unmountComponentAtNode(node); + + ReactComponentTreeDevtool.purgeUnmountedComponents(); + expect(getActualTree()).toBe(undefined); + expect(getRootDisplayNames()).toEqual([]); + expect(getRegisteredDisplayNames()).toEqual([]); + + pairs.forEach(([element, expectedTree]) => { + currentElement = element; + ReactDOMServer.renderToString(); + expect(getActualTree()).toEqual(expectedTree); + + ReactComponentTreeDevtool.purgeUnmountedComponents(); + expect(getActualTree()).toBe(undefined); + expect(getRootDisplayNames()).toEqual([]); + expect(getRegisteredDisplayNames()).toEqual([]); + }); + } + + describe('mount', () => { + it('uses displayName or Unknown for classic components', () => { + var Foo = React.createClass({ + render() { + return null; + }, + }); + Foo.displayName = 'Bar'; + var Baz = React.createClass({ + render() { + return null; + }, + }); + var Qux = React.createClass({ + render() { + return null; + }, + }); + delete Qux.displayName; + + var element =
; + var tree = { + displayName: 'div', + children: [{ + displayName: 'Bar', + children: [], + }, { + displayName: 'Baz', + children: [], + }, { + displayName: 'Unknown', + children: [], + }], + }; + assertTreeMatches([element, tree]); + }); + + it('uses displayName, name, or ReactComponent for modern components', () => { + class Foo extends React.Component { + render() { + return null; + } + } + Foo.displayName = 'Bar'; + class Baz extends React.Component { + render() { + return null; + } + } + class Qux extends React.Component { + render() { + return null; + } + } + delete Qux.name; + + var element =
; + var tree = { + displayName: 'div', + children: [{ + displayName: 'Bar', + children: [], + }, { + displayName: 'Baz', + children: [], + }, { + // Note: Ideally fallback name should be consistent (e.g. "Unknown") + displayName: 'ReactComponent', + children: [], + }], + }; + assertTreeMatches([element, tree]); + }); + + it('uses displayName, name, or Object for factory components', () => { + function Foo() { + return { + render() { + return null; + }, + }; + } + Foo.displayName = 'Bar'; + function Baz() { + return { + render() { + return null; + }, + }; + } + function Qux() { + return { + render() { + return null; + }, + }; + } + delete Qux.name; + + var element =
; + var tree = { + displayName: 'div', + children: [{ + displayName: 'Bar', + children: [], + }, { + displayName: 'Baz', + children: [], + }, { + displayName: 'Unknown', + children: [], + }], + }; + assertTreeMatches([element, tree]); + }); + + it('uses displayName, name, or StatelessComponent for functional components', () => { + function Foo() { + return null; + } + Foo.displayName = 'Bar'; + function Baz() { + return null; + } + function Qux() { + return null; + } + delete Qux.name; + + var element =
; + var tree = { + displayName: 'div', + children: [{ + displayName: 'Bar', + children: [], + }, { + displayName: 'Baz', + children: [], + }, { + displayName: 'Unknown', + children: [], + }], + }; + assertTreeMatches([element, tree]); + }); + + it('reports a native tree correctly', () => { + var element = ( +
+

+ + Hi! + + Wow. +

+
+
+ ); + var tree = { + displayName: 'div', + children: [{ + displayName: 'p', + children: [{ + displayName: 'span', + children: [{ + displayName: '#text', + text: 'Hi!', + }], + }, { + displayName: '#text', + text: 'Wow.', + }], + }, { + displayName: 'hr', + children: [], + }], + }; + assertTreeMatches([element, tree]); + }); + + it('reports a simple tree with composites correctly', () => { + class Foo extends React.Component { + render() { + return
; + } + } + + var element = ; + var tree = { + displayName: 'Foo', + children: [{ + displayName: 'div', + children: [], + }], + }; + assertTreeMatches([element, tree]); + }); + + it('reports a tree with composites correctly', () => { + var Qux = React.createClass({ + render() { + return null; + }, + }); + function Foo() { + return { + render() { + return ; + }, + }; + } + function Bar({children}) { + return

{children}

; + } + class Baz extends React.Component { + render() { + return ( +
+ + + Hi, + Mom + + Click me. +
+ ); + } + } + + var element = ; + var tree = { + displayName: 'Baz', + children: [{ + displayName: 'div', + children: [{ + displayName: 'Foo', + children: [{ + displayName: 'Qux', + children: [], + }], + }, { + displayName: 'Bar', + children: [{ + displayName: 'h1', + children: [{ + displayName: 'span', + children: [{ + displayName: '#text', + text: 'Hi,', + }], + }, { + displayName: '#text', + text: 'Mom', + }], + }], + }, { + displayName: 'a', + children: [{ + displayName: '#text', + text: 'Click me.', + }], + }], + }], + }; + assertTreeMatches([element, tree]); + }); + + it('ignores null children', () => { + class Foo extends React.Component { + render() { + return null; + } + } + var element = ; + var tree = { + displayName: 'Foo', + children: [], + }; + assertTreeMatches([element, tree]); + }); + + it('ignores false children', () => { + class Foo extends React.Component { + render() { + return false; + } + } + var element = ; + var tree = { + displayName: 'Foo', + children: [], + }; + assertTreeMatches([element, tree]); + }); + + it('reports text nodes as children', () => { + var element =
{'1'}{2}
; + var tree = { + displayName: 'div', + children: [{ + displayName: '#text', + text: '1', + }, { + displayName: '#text', + text: '2', + }], + }; + assertTreeMatches([element, tree]); + }); + + it('reports a single text node as a child', () => { + var element =
{'1'}
; + var tree = { + displayName: 'div', + children: [{ + displayName: '#text', + text: '1', + }], + }; + assertTreeMatches([element, tree]); + }); + + it('reports a single number node as a child', () => { + var element =
{42}
; + var tree = { + displayName: 'div', + children: [{ + displayName: '#text', + text: '42', + }], + }; + assertTreeMatches([element, tree]); + }); + + it('reports a zero as a child', () => { + var element =
{0}
; + var tree = { + displayName: 'div', + children: [{ + displayName: '#text', + text: '0', + }], + }; + assertTreeMatches([element, tree]); + }); + + it('skips empty nodes for multiple children', () => { + function Foo() { + return
; + } + var element = ( +
+ {'hi'} + {false} + {42} + {null} + +
+ ); + var tree = { + displayName: 'div', + children: [{ + displayName: '#text', + text: 'hi', + }, { + displayName: '#text', + text: '42', + }, { + displayName: 'Foo', + children: [{ + displayName: 'div', + children: [], + }], + }], + }; + assertTreeMatches([element, tree]); + }); + + it('reports html content as no children', () => { + var element =
; + var tree = { + displayName: 'div', + children: [], + }; + assertTreeMatches([element, tree]); + }); + }); + + describe('update', () => { + describe('native component', () => { + it('updates text of a single text child', () => { + var elementBefore =
Hi.
; + var treeBefore = { + displayName: 'div', + children: [{ + displayName: '#text', + text: 'Hi.', + }], + }; + + var elementAfter =
Bye.
; + var treeAfter = { + displayName: 'div', + children: [{ + displayName: '#text', + text: 'Bye.', + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from no children to a single text child', () => { + var elementBefore =
; + var treeBefore = { + displayName: 'div', + children: [], + }; + + var elementAfter =
Hi.
; + var treeAfter = { + displayName: 'div', + children: [{ + displayName: '#text', + text: 'Hi.', + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from a single text child to no children', () => { + var elementBefore =
Hi.
; + var treeBefore = { + displayName: 'div', + children: [{ + displayName: '#text', + text: 'Hi.', + }], + }; + + var elementAfter =
; + var treeAfter = { + displayName: 'div', + children: [], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from html content to a single text child', () => { + var elementBefore =
; + var treeBefore = { + displayName: 'div', + children: [], + }; + + var elementAfter =
Hi.
; + var treeAfter = { + displayName: 'div', + children: [{ + displayName: '#text', + text: 'Hi.', + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from a single text child to html content', () => { + var elementBefore =
Hi.
; + var treeBefore = { + displayName: 'div', + children: [{ + displayName: '#text', + text: 'Hi.', + }], + }; + + var elementAfter =
; + var treeAfter = { + displayName: 'div', + children: [], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from no children to multiple text children', () => { + var elementBefore =
; + var treeBefore = { + displayName: 'div', + children: [], + }; + + var elementAfter =
{'Hi.'}{'Bye.'}
; + var treeAfter = { + displayName: 'div', + children: [{ + displayName: '#text', + text: 'Hi.', + }, { + displayName: '#text', + text: 'Bye.', + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from multiple text children to no children', () => { + var elementBefore =
{'Hi.'}{'Bye.'}
; + var treeBefore = { + displayName: 'div', + children: [{ + displayName: '#text', + text: 'Hi.', + }, { + displayName: '#text', + text: 'Bye.', + }], + }; + + var elementAfter =
; + var treeAfter = { + displayName: 'div', + children: [], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from html content to multiple text children', () => { + var elementBefore =
; + var treeBefore = { + displayName: 'div', + children: [], + }; + + var elementAfter =
{'Hi.'}{'Bye.'}
; + var treeAfter = { + displayName: 'div', + children: [{ + displayName: '#text', + text: 'Hi.', + }, { + displayName: '#text', + text: 'Bye.', + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from multiple text children to html content', () => { + var elementBefore =
{'Hi.'}{'Bye.'}
; + var treeBefore = { + displayName: 'div', + children: [{ + displayName: '#text', + text: 'Hi.', + }, { + displayName: '#text', + text: 'Bye.', + }], + }; + + var elementAfter =
; + var treeAfter = { + displayName: 'div', + children: [], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from html content to no children', () => { + var elementBefore =
; + var treeBefore = { + displayName: 'div', + children: [], + }; + + var elementAfter =
; + var treeAfter = { + displayName: 'div', + children: [], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from no children to html content', () => { + var elementBefore =
; + var treeBefore = { + displayName: 'div', + children: [], + }; + + var elementAfter =
; + var treeAfter = { + displayName: 'div', + children: [], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from one text child to multiple text children', () => { + var elementBefore =
Hi.
; + var treeBefore = { + displayName: 'div', + children: [{ + displayName: '#text', + text: 'Hi.', + }], + }; + + var elementAfter =
{'Hi.'}{'Bye.'}
; + var treeAfter = { + displayName: 'div', + children: [{ + displayName: '#text', + text: 'Hi.', + }, { + displayName: '#text', + text: 'Bye.', + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from multiple text children to one text child', () => { + var elementBefore =
{'Hi.'}{'Bye.'}
; + var treeBefore = { + displayName: 'div', + children: [{ + displayName: '#text', + text: 'Hi.', + }, { + displayName: '#text', + text: 'Bye.', + }], + }; + + var elementAfter =
Hi.
; + var treeAfter = { + displayName: 'div', + children: [{ + displayName: '#text', + text: 'Hi.', + }], + }; + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates text nodes when reordering', () => { + var elementBefore =
{'Hi.'}{'Bye.'}
; + var treeBefore = { + displayName: 'div', + children: [{ + displayName: '#text', + text: 'Hi.', + }, { + displayName: '#text', + text: 'Bye.', + }], + }; + + var elementAfter =
{'Bye.'}{'Hi.'}
; + var treeAfter = { + displayName: 'div', + children: [{ + displayName: '#text', + text: 'Bye.', + }, { + displayName: '#text', + text: 'Hi.', + }], + }; + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates native nodes when reordering with keys', () => { + var elementBefore = ( +
+
Hi.
+
Bye.
+
+ ); + var treeBefore = { + displayName: 'div', + children: [{ + displayName: 'div', + children: [{ + displayName: '#text', + text: 'Hi.', + }], + }, { + displayName: 'div', + children: [{ + displayName: '#text', + text: 'Bye.', + }], + }], + }; + + var elementAfter = ( +
+
Bye.
+
Hi.
+
+ ); + var treeAfter = { + displayName: 'div', + children: [{ + displayName: 'div', + children: [{ + displayName: '#text', + text: 'Bye.', + }], + }, { + displayName: 'div', + children: [{ + displayName: '#text', + text: 'Hi.', + }], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates native nodes when reordering without keys', () => { + var elementBefore = ( +
+
Hi.
+
Bye.
+
+ ); + var treeBefore = { + displayName: 'div', + children: [{ + displayName: 'div', + children: [{ + displayName: '#text', + text: 'Hi.', + }], + }, { + displayName: 'div', + children: [{ + displayName: '#text', + text: 'Bye.', + }], + }], + }; + + var elementAfter = ( +
+
Bye.
+
Hi.
+
+ ); + var treeAfter = { + displayName: 'div', + children: [{ + displayName: 'div', + children: [{ + displayName: '#text', + text: 'Bye.', + }], + }, { + displayName: 'div', + children: [{ + displayName: '#text', + text: 'Hi.', + }], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates a single composite child of a different type', () => { + function Foo() { + return null; + } + + function Bar() { + return null; + } + + var elementBefore =
; + var treeBefore = { + displayName: 'div', + children: [{ + displayName: 'Foo', + children: [], + }], + }; + + var elementAfter =
; + var treeAfter = { + displayName: 'div', + children: [{ + displayName: 'Bar', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates a single composite child of the same type', () => { + function Foo({ children }) { + return children; + } + + var elementBefore =
; + var treeBefore = { + displayName: 'div', + children: [{ + displayName: 'Foo', + children: [{ + displayName: 'div', + children: [], + }], + }], + }; + + var elementAfter =
; + var treeAfter = { + displayName: 'div', + children: [{ + displayName: 'Foo', + children: [{ + displayName: 'span', + children: [], + }], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from no children to a single composite child', () => { + function Foo() { + return null; + } + + var elementBefore =
; + var treeBefore = { + displayName: 'div', + children: [], + }; + + var elementAfter =
; + var treeAfter = { + displayName: 'div', + children: [{ + displayName: 'Foo', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from a single composite child to no children', () => { + function Foo() { + return null; + } + + var elementBefore =
; + var treeBefore = { + displayName: 'div', + children: [{ + displayName: 'Foo', + children: [], + }], + }; + + var elementAfter =
; + var treeAfter = { + displayName: 'div', + children: [], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates mixed children', () => { + function Foo() { + return
; + } + var element1 = ( +
+ {'hi'} + {false} + {42} + {null} + +
+ ); + var tree1 = { + displayName: 'div', + children: [{ + displayName: '#text', + text: 'hi', + }, { + displayName: '#text', + text: '42', + }, { + displayName: 'Foo', + children: [{ + displayName: 'div', + children: [], + }], + }], + }; + + var element2 = ( +
+ + {false} + {'hi'} + {null} +
+ ); + var tree2 = { + displayName: 'div', + children: [{ + displayName: 'Foo', + children: [{ + displayName: 'div', + children: [], + }], + }, { + displayName: '#text', + text: 'hi', + }], + }; + + var element3 = ( +
+ +
+ ); + var tree3 = { + displayName: 'div', + children: [{ + displayName: 'Foo', + children: [{ + displayName: 'div', + children: [], + }], + }], + }; + + assertTreeMatches([ + [element1, tree1], + [element2, tree2], + [element3, tree3], + ]); + }); + }); + + describe('functional component', () => { + it('updates with a native child', () => { + function Foo({ children }) { + return children; + } + + var elementBefore =
; + var treeBefore = { + displayName: 'Foo', + children: [{ + displayName: 'div', + children: [], + }], + }; + + var elementAfter = ; + var treeAfter = { + displayName: 'Foo', + children: [{ + displayName: 'span', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from null to a native child', () => { + function Foo({ children }) { + return children; + } + + var elementBefore = {null}; + var treeBefore = { + displayName: 'Foo', + children: [], + }; + + var elementAfter =
; + var treeAfter = { + displayName: 'Foo', + children: [{ + displayName: 'div', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from a native child to null', () => { + function Foo({ children }) { + return children; + } + + var elementBefore =
; + var treeBefore = { + displayName: 'Foo', + children: [{ + displayName: 'div', + children: [], + }], + }; + + var elementAfter = {null}; + var treeAfter = { + displayName: 'Foo', + children: [], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from a native child to a composite child', () => { + function Bar() { + return null; + } + + function Foo({ children }) { + return children; + } + + var elementBefore =
; + var treeBefore = { + displayName: 'Foo', + children: [{ + displayName: 'div', + children: [], + }], + }; + + var elementAfter = ; + var treeAfter = { + displayName: 'Foo', + children: [{ + displayName: 'Bar', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from a composite child to a native child', () => { + function Bar() { + return null; + } + + function Foo({ children }) { + return children; + } + + var elementBefore = ; + var treeBefore = { + displayName: 'Foo', + children: [{ + displayName: 'Bar', + children: [], + }], + }; + + var elementAfter =
; + var treeAfter = { + displayName: 'Foo', + children: [{ + displayName: 'div', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from null to a composite child', () => { + function Bar() { + return null; + } + + function Foo({ children }) { + return children; + } + + var elementBefore = {null}; + var treeBefore = { + displayName: 'Foo', + children: [], + }; + + var elementAfter = ; + var treeAfter = { + displayName: 'Foo', + children: [{ + displayName: 'Bar', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from a composite child to null', () => { + function Bar() { + return null; + } + + function Foo({ children }) { + return children; + } + + var elementBefore = ; + var treeBefore = { + displayName: 'Foo', + children: [{ + displayName: 'Bar', + children: [], + }], + }; + + var elementAfter = {null}; + var treeAfter = { + displayName: 'Foo', + children: [], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + }); + + describe('class component', () => { + it('updates with a native child', () => { + var Foo = React.createClass({ + render() { + return this.props.children; + }, + }); + + var elementBefore =
; + var treeBefore = { + displayName: 'Foo', + children: [{ + displayName: 'div', + children: [], + }], + }; + + var elementAfter = ; + var treeAfter = { + displayName: 'Foo', + children: [{ + displayName: 'span', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from null to a native child', () => { + var Foo = React.createClass({ + render() { + return this.props.children; + }, + }); + + var elementBefore = {null}; + var treeBefore = { + displayName: 'Foo', + children: [], + }; + + var elementAfter =
; + var treeAfter = { + displayName: 'Foo', + children: [{ + displayName: 'div', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from a native child to null', () => { + var Foo = React.createClass({ + render() { + return this.props.children; + }, + }); + + var elementBefore =
; + var treeBefore = { + displayName: 'Foo', + children: [{ + displayName: 'div', + children: [], + }], + }; + + var elementAfter = {null}; + var treeAfter = { + displayName: 'Foo', + children: [], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from a native child to a composite child', () => { + var Bar = React.createClass({ + render() { + return null; + }, + }); + + var Foo = React.createClass({ + render() { + return this.props.children; + }, + }); + + var elementBefore =
; + var treeBefore = { + displayName: 'Foo', + children: [{ + displayName: 'div', + children: [], + }], + }; + + var elementAfter = ; + var treeAfter = { + displayName: 'Foo', + children: [{ + displayName: 'Bar', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from a composite child to a native child', () => { + var Bar = React.createClass({ + render() { + return null; + }, + }); + + var Foo = React.createClass({ + render() { + return this.props.children; + }, + }); + + var elementBefore = ; + var treeBefore = { + displayName: 'Foo', + children: [{ + displayName: 'Bar', + children: [], + }], + }; + + var elementAfter =
; + var treeAfter = { + displayName: 'Foo', + children: [{ + displayName: 'div', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from null to a composite child', () => { + var Bar = React.createClass({ + render() { + return null; + }, + }); + + var Foo = React.createClass({ + render() { + return this.props.children; + }, + }); + + var elementBefore = {null}; + var treeBefore = { + displayName: 'Foo', + children: [], + }; + + var elementAfter = ; + var treeAfter = { + displayName: 'Foo', + children: [{ + displayName: 'Bar', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from a composite child to null', () => { + var Bar = React.createClass({ + render() { + return null; + }, + }); + + var Foo = React.createClass({ + render() { + return this.props.children; + }, + }); + + var elementBefore = ; + var treeBefore = { + displayName: 'Foo', + children: [{ + displayName: 'Bar', + children: [], + }], + }; + + var elementAfter = {null}; + var treeAfter = { + displayName: 'Foo', + children: [], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + }); + }); + + it('tracks owner correctly', () => { + class Foo extends React.Component { + render() { + return

Hi.

; + } + } + function Bar({children}) { + return
{children} Mom
; + } + + // Note that owner is not calculated for text nodes + // because they are not created from real elements. + var element =
; + var tree = { + displayName: 'article', + children: [{ + displayName: 'Foo', + children: [{ + displayName: 'Bar', + ownerDisplayName: 'Foo', + children: [{ + displayName: 'div', + ownerDisplayName: 'Bar', + children: [{ + displayName: 'h1', + ownerDisplayName: 'Foo', + children: [{ + displayName: '#text', + text: 'Hi.', + }], + }, { + displayName: '#text', + text: ' Mom', + }], + }], + }], + }], + }; + assertTreeMatches([element, tree], {includeOwnerDisplayName: true}); + }); + + it('preserves unmounted components until purge', () => { + var node = document.createElement('div'); + var renderBar = true; + var fooInstance; + var barInstance; + + class Foo extends React.Component { + render() { + fooInstance = ReactInstanceMap.get(this); + return renderBar ? : null; + } + } + + class Bar extends React.Component { + render() { + barInstance = ReactInstanceMap.get(this); + return null; + } + } + + ReactDOM.render(, node); + expect( + getTree(barInstance._debugID, { + includeParentDisplayName: true, + expectedParentID: fooInstance._debugID, + }) + ).toEqual({ + displayName: 'Bar', + parentDisplayName: 'Foo', + children: [], + }); + + renderBar = false; + ReactDOM.render(, node); + expect( + getTree(barInstance._debugID, { + includeParentDisplayName: true, + expectedParentID: fooInstance._debugID, + }) + ).toEqual({ + displayName: 'Bar', + parentDisplayName: 'Foo', + children: [], + }); + + ReactDOM.unmountComponentAtNode(node); + expect( + getTree(barInstance._debugID, { + includeParentDisplayName: true, + expectedParentID: fooInstance._debugID, + }) + ).toEqual({ + displayName: 'Bar', + parentDisplayName: 'Foo', + children: [], + }); + + ReactComponentTreeDevtool.purgeUnmountedComponents(); + expect( + getTree(barInstance._debugID, {includeParentDisplayName: true}) + ).toEqual({ + displayName: 'Unknown', + children: [], + }); + }); + + it('does not report top-level wrapper as a root', () => { + var node = document.createElement('div'); + + ReactDOM.render(
, node); + expect(getRootDisplayNames()).toEqual(['div']); + + ReactDOM.render(
, node); + expect(getRootDisplayNames()).toEqual(['div']); + + ReactDOM.unmountComponentAtNode(node); + expect(getRootDisplayNames()).toEqual([]); + + ReactComponentTreeDevtool.purgeUnmountedComponents(); + expect(getRootDisplayNames()).toEqual([]); + + // This currently contains TopLevelWrapper until purge + // so we only check it at the very end. + expect(getRegisteredDisplayNames()).toEqual([]); + }); +}); diff --git a/src/renderers/dom/client/ReactMount.js b/src/renderers/dom/client/ReactMount.js index 7f6f8cac6ff5f..1984e146f3f27 100644 --- a/src/renderers/dom/client/ReactMount.js +++ b/src/renderers/dom/client/ReactMount.js @@ -333,6 +333,12 @@ var ReactMount = { ReactBrowserEventEmitter.ensureScrollValueMonitoring(); var componentInstance = instantiateReactComponent(nextElement); + if (__DEV__) { + // Mute future events from the top level wrapper. + // It is an implementation detail that devtools should not know about. + componentInstance._debugID = 0; + } + // The initial render is synchronous but any updates that happen during // rendering, in componentWillMount or componentDidMount, will be batched // according to the current batching strategy. @@ -349,7 +355,10 @@ var ReactMount = { instancesByReactRootID[wrapperID] = componentInstance; if (__DEV__) { - ReactInstrumentation.debugTool.onMountRootComponent(componentInstance); + // The instance here is TopLevelWrapper so we report mount for its child. + ReactInstrumentation.debugTool.onMountRootComponent( + componentInstance._renderedComponent._debugID + ); } return componentInstance; diff --git a/src/renderers/dom/server/ReactServerRendering.js b/src/renderers/dom/server/ReactServerRendering.js index 5a5d604b7dd8d..2d30f1a18d4ae 100644 --- a/src/renderers/dom/server/ReactServerRendering.js +++ b/src/renderers/dom/server/ReactServerRendering.js @@ -13,7 +13,9 @@ var ReactDOMContainerInfo = require('ReactDOMContainerInfo'); var ReactDefaultBatchingStrategy = require('ReactDefaultBatchingStrategy'); var ReactElement = require('ReactElement'); +var ReactInstrumentation = require('ReactInstrumentation'); var ReactMarkupChecksum = require('ReactMarkupChecksum'); +var ReactReconciler = require('ReactReconciler'); var ReactServerBatchingStrategy = require('ReactServerBatchingStrategy'); var ReactServerRenderingTransaction = require('ReactServerRenderingTransaction'); @@ -36,12 +38,18 @@ function renderToStringImpl(element, makeStaticMarkup) { return transaction.perform(function() { var componentInstance = instantiateReactComponent(element); - var markup = componentInstance.mountComponent( + var markup = ReactReconciler.mountComponent( + componentInstance, transaction, null, ReactDOMContainerInfo(), emptyObject ); + if (__DEV__) { + ReactInstrumentation.debugTool.onUnmountComponent( + componentInstance._debugID + ); + } if (!makeStaticMarkup) { markup = ReactMarkupChecksum.addChecksumToMarkup(markup); } diff --git a/src/renderers/dom/shared/ReactDOMComponent.js b/src/renderers/dom/shared/ReactDOMComponent.js index 5a7cf9ff11ce2..d2c77fe1cd7b7 100644 --- a/src/renderers/dom/shared/ReactDOMComponent.js +++ b/src/renderers/dom/shared/ReactDOMComponent.js @@ -32,9 +32,11 @@ var ReactDOMInput = require('ReactDOMInput'); var ReactDOMOption = require('ReactDOMOption'); var ReactDOMSelect = require('ReactDOMSelect'); var ReactDOMTextarea = require('ReactDOMTextarea'); +var ReactInstrumentation = require('ReactInstrumentation'); var ReactMultiChild = require('ReactMultiChild'); var ReactPerf = require('ReactPerf'); +var emptyFunction = require('emptyFunction'); var escapeTextContentForBrowser = require('escapeTextContentForBrowser'); var invariant = require('invariant'); var isEventSupported = require('isEventSupported'); @@ -245,6 +247,19 @@ function optionPostMount() { ReactDOMOption.postMountWrapper(inst); } +var setContentChildForInstrumentation = emptyFunction; +if (__DEV__) { + setContentChildForInstrumentation = function(contentToUse) { + var debugID = this._debugID; + var contentDebugID = debugID + '#text'; + this._contentDebugID = contentDebugID; + ReactInstrumentation.debugTool.onSetDisplayName(contentDebugID, '#text'); + ReactInstrumentation.debugTool.onSetText(contentDebugID, '' + contentToUse); + ReactInstrumentation.debugTool.onMountComponent(contentDebugID); + ReactInstrumentation.debugTool.onSetChildren(debugID, [contentDebugID]); + }; +} + // There are so many media events, it makes sense to just // maintain a list rather than create a `trapBubbledEvent` for each var mediaEvents = { @@ -452,6 +467,7 @@ function ReactDOMComponent(element) { this._flags = 0; if (__DEV__) { this._ancestorInfo = null; + this._contentDebugID = null; } } @@ -717,6 +733,9 @@ ReactDOMComponent.Mixin = { if (contentToUse != null) { // TODO: Validate that text is allowed as a child of this node ret = escapeTextContentForBrowser(contentToUse); + if (__DEV__) { + setContentChildForInstrumentation.call(this, contentToUse); + } } else if (childrenToUse != null) { var mountImages = this.mountChildren( childrenToUse, @@ -756,6 +775,9 @@ ReactDOMComponent.Mixin = { var childrenToUse = contentToUse != null ? null : props.children; if (contentToUse != null) { // TODO: Validate that text is allowed as a child of this node + if (__DEV__) { + setContentChildForInstrumentation.call(this, contentToUse); + } DOMLazyTree.queueText(lazyTree, contentToUse); } else if (childrenToUse != null) { var mountImages = this.mountChildren( @@ -1003,17 +1025,34 @@ ReactDOMComponent.Mixin = { this.updateChildren(null, transaction, context); } else if (lastHasContentOrHtml && !nextHasContentOrHtml) { this.updateTextContent(''); + if (__DEV__) { + ReactInstrumentation.debugTool.onSetChildren(this._debugID, []); + } } if (nextContent != null) { if (lastContent !== nextContent) { this.updateTextContent('' + nextContent); + if (__DEV__) { + this._contentDebugID = this._debugID + '#text'; + setContentChildForInstrumentation.call(this, nextContent); + } } } else if (nextHtml != null) { if (lastHtml !== nextHtml) { this.updateMarkup('' + nextHtml); } + if (__DEV__) { + ReactInstrumentation.debugTool.onSetChildren(this._debugID, []); + } } else if (nextChildren != null) { + if (__DEV__) { + if (this._contentDebugID) { + ReactInstrumentation.debugTool.onUnmountComponent(this._contentDebugID); + this._contentDebugID = null; + } + } + this.updateChildren(nextChildren, transaction, context); } }, @@ -1075,6 +1114,11 @@ ReactDOMComponent.Mixin = { this._rootNodeID = null; this._domID = null; this._wrapperState = null; + + if (this._contentDebugID) { + ReactInstrumentation.debugTool.onUnmountComponent(this._contentDebugID); + this._contentDebugID = null; + } }, getPublicInstance: function() { diff --git a/src/renderers/dom/shared/ReactDOMTextComponent.js b/src/renderers/dom/shared/ReactDOMTextComponent.js index 9a69c7f97762a..5041706287355 100644 --- a/src/renderers/dom/shared/ReactDOMTextComponent.js +++ b/src/renderers/dom/shared/ReactDOMTextComponent.js @@ -14,6 +14,7 @@ var DOMChildrenOperations = require('DOMChildrenOperations'); var DOMLazyTree = require('DOMLazyTree'); var ReactDOMComponentTree = require('ReactDOMComponentTree'); +var ReactInstrumentation = require('ReactInstrumentation'); var ReactPerf = require('ReactPerf'); var escapeTextContentForBrowser = require('escapeTextContentForBrowser'); @@ -67,6 +68,8 @@ Object.assign(ReactDOMTextComponent.prototype, { context ) { if (__DEV__) { + ReactInstrumentation.debugTool.onSetText(this._debugID, this._stringText); + var parentInfo; if (nativeParent != null) { parentInfo = nativeParent._ancestorInfo; @@ -140,6 +143,10 @@ Object.assign(ReactDOMTextComponent.prototype, { commentNodes[1], nextStringText ); + + if (__DEV__) { + ReactInstrumentation.debugTool.onSetText(this._debugID, nextStringText); + } } } }, diff --git a/src/renderers/shared/reconciler/ReactCompositeComponent.js b/src/renderers/shared/reconciler/ReactCompositeComponent.js index 907de98089ec0..26d1c8fb6e709 100644 --- a/src/renderers/shared/reconciler/ReactCompositeComponent.js +++ b/src/renderers/shared/reconciler/ReactCompositeComponent.js @@ -386,6 +386,17 @@ var ReactCompositeComponentMixin = { this._processChildContext(context) ); + if (__DEV__) { + if (this._debugID !== 0) { + ReactInstrumentation.debugTool.onSetChildren( + this._debugID, + this._renderedComponent._debugID !== 0 ? + [this._renderedComponent._debugID] : + [] + ); + } + } + return markup; }, @@ -853,6 +864,7 @@ var ReactCompositeComponentMixin = { this._renderedComponent = this._instantiateReactComponent( nextRenderedElement ); + var nextMarkup = ReactReconciler.mountComponent( this._renderedComponent, transaction, @@ -860,6 +872,18 @@ var ReactCompositeComponentMixin = { this._nativeContainerInfo, this._processChildContext(context) ); + + if (__DEV__) { + if (this._debugID !== 0) { + ReactInstrumentation.debugTool.onSetChildren( + this._debugID, + this._renderedComponent._debugID !== 0 ? + [this._renderedComponent._debugID] : + [] + ); + } + } + this._replaceNodeWithMarkup(oldNativeNode, nextMarkup); } }, diff --git a/src/renderers/shared/reconciler/ReactMultiChild.js b/src/renderers/shared/reconciler/ReactMultiChild.js index 66f38613218be..474ddc63dc3ef 100644 --- a/src/renderers/shared/reconciler/ReactMultiChild.js +++ b/src/renderers/shared/reconciler/ReactMultiChild.js @@ -12,12 +12,14 @@ 'use strict'; var ReactComponentEnvironment = require('ReactComponentEnvironment'); +var ReactInstrumentation = require('ReactInstrumentation'); var ReactMultiChildUpdateTypes = require('ReactMultiChildUpdateTypes'); var ReactCurrentOwner = require('ReactCurrentOwner'); var ReactReconciler = require('ReactReconciler'); var ReactChildReconciler = require('ReactChildReconciler'); +var emptyFunction = require('emptyFunction'); var flattenChildren = require('flattenChildren'); var invariant = require('invariant'); @@ -137,6 +139,16 @@ function processQueue(inst, updateQueue) { ); } +var setChildrenForInstrumentation = emptyFunction; +if (__DEV__) { + setChildrenForInstrumentation = function(children) { + ReactInstrumentation.debugTool.onSetChildren( + this._debugID, + children ? Object.keys(children).map(key => children[key]._debugID) : [] + ); + }; +} + /** * ReactMultiChild are capable of reconciling multiple children. * @@ -214,6 +226,7 @@ var ReactMultiChild = { nestedChildren, transaction, context ); this._renderedChildren = children; + var mountImages = []; var index = 0; for (var name in children) { @@ -230,6 +243,11 @@ var ReactMultiChild = { mountImages.push(mountImage); } } + + if (__DEV__) { + setChildrenForInstrumentation.call(this, children); + } + return mountImages; }, @@ -357,6 +375,10 @@ var ReactMultiChild = { processQueue(this, updates); } this._renderedChildren = nextChildren; + + if (__DEV__) { + setChildrenForInstrumentation.call(this, nextChildren); + } }, /** diff --git a/src/renderers/shared/reconciler/ReactReconciler.js b/src/renderers/shared/reconciler/ReactReconciler.js index ae7806e4fc0ed..d2a2dbcac851e 100644 --- a/src/renderers/shared/reconciler/ReactReconciler.js +++ b/src/renderers/shared/reconciler/ReactReconciler.js @@ -53,7 +53,9 @@ var ReactReconciler = { transaction.getReactMountReady().enqueue(attachRefs, internalInstance); } if (__DEV__) { - ReactInstrumentation.debugTool.onMountComponent(internalInstance); + if (internalInstance._debugID !== 0) { + ReactInstrumentation.debugTool.onMountComponent(internalInstance._debugID); + } } return markup; }, @@ -76,7 +78,9 @@ var ReactReconciler = { ReactRef.detachRefs(internalInstance, internalInstance._currentElement); internalInstance.unmountComponent(safely); if (__DEV__) { - ReactInstrumentation.debugTool.onUnmountComponent(internalInstance); + if (internalInstance._debugID !== 0) { + ReactInstrumentation.debugTool.onUnmountComponent(internalInstance._debugID); + } } }, @@ -128,7 +132,9 @@ var ReactReconciler = { } if (__DEV__) { - ReactInstrumentation.debugTool.onUpdateComponent(internalInstance); + if (internalInstance._debugID !== 0) { + ReactInstrumentation.debugTool.onUpdateComponent(internalInstance._debugID); + } } }, @@ -145,7 +151,9 @@ var ReactReconciler = { ) { internalInstance.performUpdateIfNecessary(transaction); if (__DEV__) { - ReactInstrumentation.debugTool.onUpdateComponent(internalInstance); + if (internalInstance._debugID !== 0) { + ReactInstrumentation.debugTool.onUpdateComponent(internalInstance._debugID); + } } }, diff --git a/src/renderers/shared/reconciler/instantiateReactComponent.js b/src/renderers/shared/reconciler/instantiateReactComponent.js index 796e037b9958b..db22ea3b91df7 100644 --- a/src/renderers/shared/reconciler/instantiateReactComponent.js +++ b/src/renderers/shared/reconciler/instantiateReactComponent.js @@ -14,6 +14,7 @@ var ReactCompositeComponent = require('ReactCompositeComponent'); var ReactEmptyComponent = require('ReactEmptyComponent'); var ReactNativeComponent = require('ReactNativeComponent'); +var ReactInstrumentation = require('ReactInstrumentation'); var invariant = require('invariant'); var warning = require('warning'); @@ -40,6 +41,21 @@ function getDeclarationErrorAddendum(owner) { return ''; } +function getDisplayName(instance) { + var element = instance._currentElement; + if (element == null) { + return '#empty'; + } else if (typeof element === 'string' || typeof element === 'number') { + return '#text'; + } else if (typeof element.type === 'string') { + return element.type; + } else if (instance.getName) { + return instance.getName() || 'Unknown'; + } else { + return element.type.displayName || element.type.name || 'Unknown'; + } +} + /** * Check if the type reference is a known internal type. I.e. not a user * provided composite type. @@ -56,6 +72,8 @@ function isInternalComponentType(type) { ); } +var nextDebugID = 1; + /** * Given a ReactNode, create an instance that will actually be mounted. * @@ -66,7 +84,8 @@ function isInternalComponentType(type) { function instantiateReactComponent(node) { var instance; - if (node === null || node === false) { + var isEmpty = node === null || node === false; + if (isEmpty) { instance = ReactEmptyComponent.create(instantiateReactComponent); } else if (typeof node === 'object') { var element = node; @@ -121,6 +140,18 @@ function instantiateReactComponent(node) { instance._warnedAboutRefsInRender = false; } + if (__DEV__) { + var debugID = isEmpty ? 0 : nextDebugID++; + instance._debugID = debugID; + + var displayName = getDisplayName(instance); + ReactInstrumentation.debugTool.onSetDisplayName(debugID, displayName); + var owner = node && node._owner; + if (owner) { + ReactInstrumentation.debugTool.onSetOwner(debugID, owner._debugID); + } + } + // Internal instances should fully constructed at this point, so they should // not get any new fields added to them at this point. if (__DEV__) {