Add Debug Tools Package for Introspection of Hooks (#14085)

* Add debug tools package

* Add basic implementation

* Implement inspection of the current state of hooks using the fiber tree

* Support useContext hooks inspection by backtracking from the Fiber

I'm not sure this is safe because the return fibers may not be current
but close enough and it's fast.

We use this to set up the current values of the providers.

* rm copypasta

* Use lastIndexOf

Just in case. I don't know of any scenario where this can happen.

* Support ForwardRef

* Add test for memo and custom hooks

* Support defaultProps resolution
This commit is contained in:
Sebastian Markbåge 2018-11-05 10:02:59 -08:00 committed by GitHub
parent b305c4e034
commit fd1256a561
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 1265 additions and 150 deletions

View File

@ -44,6 +44,7 @@
"create-react-class": "^15.6.3",
"cross-env": "^5.1.1",
"danger": "^3.0.4",
"error-stack-parser": "^2.0.2",
"eslint": "^4.1.0",
"eslint-config-fbjs": "^1.1.1",
"eslint-plugin-babel": "^3.3.0",

View File

@ -0,0 +1,7 @@
# react-debug-tools
This is an experimental package for debugging React renderers.
**Its API is not as stable as that of React, React Native, or React DOM, and does not follow the common versioning scheme.**
**Use it at your own risk.**

13
packages/react-debug-tools/index.js vendored Normal file
View File

@ -0,0 +1,13 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
const ReactDebugTools = require('./src/ReactDebugTools');
// This is hacky but makes it work with both Rollup and Jest.
module.exports = ReactDebugTools.default || ReactDebugTools;

View File

@ -0,0 +1,7 @@
'use strict';
if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react-debug-tools.production.min.js');
} else {
module.exports = require('./cjs/react-debug-tools.development.js');
}

View File

@ -0,0 +1,28 @@
{
"name": "react-debug-tools",
"description": "React package for debugging React trees.",
"version": "0.16.0",
"keywords": [
"react"
],
"homepage": "https://reactjs.org/",
"bugs": "https://github.com/facebook/react/issues",
"license": "MIT",
"files": [
"LICENSE",
"README.md",
"index.js",
"cjs/"
],
"main": "index.js",
"repository": "facebook/react",
"engines": {
"node": ">=0.10.0"
},
"peerDependencies": {
"react": "^16.0.0"
},
"dependencies": {
"error-stack-parser": "^2.0.2"
}
}

View File

@ -0,0 +1,530 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {ReactContext, ReactProviderType} from 'shared/ReactTypes';
import type {Fiber} from 'react-reconciler/src/ReactFiber';
import type {Hook} from 'react-reconciler/src/ReactFiberHooks';
import ErrorStackParser from 'error-stack-parser';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import {
FunctionComponent,
SimpleMemoComponent,
ContextProvider,
ForwardRef,
} from 'shared/ReactWorkTags';
const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
// Used to track hooks called during a render
type HookLogEntry = {
primitive: string,
stackError: Error,
value: mixed,
};
let hookLog: Array<HookLogEntry> = [];
// Primitives
type BasicStateAction<S> = (S => S) | S;
type Dispatch<A> = A => void;
let primitiveStackCache: null | Map<string, Array<any>> = null;
function getPrimitiveStackCache(): Map<string, Array<any>> {
// This initializes a cache of all primitive hooks so that the top
// most stack frames added by calling the primitive hook can be removed.
if (primitiveStackCache === null) {
let cache = new Map();
let readHookLog;
try {
// Use all hooks here to add them to the hook log.
Dispatcher.useContext(({_currentValue: null}: any));
Dispatcher.useState(null);
Dispatcher.useReducer((s, a) => s, null);
Dispatcher.useRef(null);
Dispatcher.useMutationEffect(() => {});
Dispatcher.useLayoutEffect(() => {});
Dispatcher.useEffect(() => {});
Dispatcher.useImperativeMethods(undefined, () => null);
Dispatcher.useCallback(() => {});
Dispatcher.useMemo(() => null);
} finally {
readHookLog = hookLog;
hookLog = [];
}
for (let i = 0; i < readHookLog.length; i++) {
let hook = readHookLog[i];
cache.set(hook.primitive, ErrorStackParser.parse(hook.stackError));
}
primitiveStackCache = cache;
}
return primitiveStackCache;
}
let currentHook: null | Hook = null;
function nextHook(): null | Hook {
let hook = currentHook;
if (hook !== null) {
currentHook = hook.next;
}
return hook;
}
function readContext<T>(
context: ReactContext<T>,
observedBits: void | number | boolean,
): T {
// For now we don't expose readContext usage in the hooks debugging info.
return context._currentValue;
}
function useContext<T>(
context: ReactContext<T>,
observedBits: void | number | boolean,
): T {
hookLog.push({
primitive: 'Context',
stackError: new Error(),
value: context._currentValue,
});
return context._currentValue;
}
function useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
let hook = nextHook();
let state: S =
hook !== null
? hook.memoizedState
: typeof initialState === 'function'
? initialState()
: initialState;
hookLog.push({primitive: 'State', stackError: new Error(), value: state});
return [state, (action: BasicStateAction<S>) => {}];
}
function useReducer<S, A>(
reducer: (S, A) => S,
initialState: S,
initialAction: A | void | null,
): [S, Dispatch<A>] {
let hook = nextHook();
let state = hook !== null ? hook.memoizedState : initialState;
hookLog.push({
primitive: 'Reducer',
stackError: new Error(),
value: state,
});
return [state, (action: A) => {}];
}
function useRef<T>(initialValue: T): {current: T} {
let hook = nextHook();
let ref = hook !== null ? hook.memoizedState : {current: initialValue};
hookLog.push({
primitive: 'Ref',
stackError: new Error(),
value: ref.current,
});
return ref;
}
function useMutationEffect(
create: () => mixed,
inputs: Array<mixed> | void | null,
): void {
nextHook();
hookLog.push({
primitive: 'MutationEffect',
stackError: new Error(),
value: create,
});
}
function useLayoutEffect(
create: () => mixed,
inputs: Array<mixed> | void | null,
): void {
nextHook();
hookLog.push({
primitive: 'LayoutEffect',
stackError: new Error(),
value: create,
});
}
function useEffect(
create: () => mixed,
inputs: Array<mixed> | void | null,
): void {
nextHook();
hookLog.push({primitive: 'Effect', stackError: new Error(), value: create});
}
function useImperativeMethods<T>(
ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
create: () => T,
inputs: Array<mixed> | void | null,
): void {
nextHook();
// We don't actually store the instance anywhere if there is no ref callback
// and if there is a ref callback it might not store it but if it does we
// have no way of knowing where. So let's only enable introspection of the
// ref itself if it is using the object form.
let instance = undefined;
if (ref !== null && typeof ref === 'object') {
instance = ref.current;
}
hookLog.push({
primitive: 'ImperativeMethods',
stackError: new Error(),
value: instance,
});
}
function useCallback<T>(callback: T, inputs: Array<mixed> | void | null): T {
let hook = nextHook();
hookLog.push({
primitive: 'Callback',
stackError: new Error(),
value: hook !== null ? hook.memoizedState[0] : callback,
});
return callback;
}
function useMemo<T>(
nextCreate: () => T,
inputs: Array<mixed> | void | null,
): T {
let hook = nextHook();
let value = hook !== null ? hook.memoizedState[0] : nextCreate();
hookLog.push({primitive: 'Memo', stackError: new Error(), value});
return value;
}
const Dispatcher = {
readContext,
useCallback,
useContext,
useEffect,
useImperativeMethods,
useLayoutEffect,
useMemo,
useMutationEffect,
useReducer,
useRef,
useState,
};
// Inspect
type HooksNode = {
name: string,
value: mixed,
subHooks: Array<HooksNode>,
};
type HooksTree = Array<HooksNode>;
// Don't assume
//
// We can't assume that stack frames are nth steps away from anything.
// E.g. we can't assume that the root call shares all frames with the stack
// of a hook call. A simple way to demonstrate this is wrapping `new Error()`
// in a wrapper constructor like a polyfill. That'll add an extra frame.
// Similar things can happen with the call to the dispatcher. The top frame
// may not be the primitive. Likewise the primitive can have fewer stack frames
// such as when a call to useState got inlined to use dispatcher.useState.
//
// We also can't assume that the last frame of the root call is the same
// frame as the last frame of the hook call because long stack traces can be
// truncated to a stack trace limit.
let mostLikelyAncestorIndex = 0;
function findSharedIndex(hookStack, rootStack, rootIndex) {
let source = rootStack[rootIndex].source;
hookSearch: for (let i = 0; i < hookStack.length; i++) {
if (hookStack[i].source === source) {
// This looks like a match. Validate that the rest of both stack match up.
for (
let a = rootIndex + 1, b = i + 1;
a < rootStack.length && b < hookStack.length;
a++, b++
) {
if (hookStack[b].source !== rootStack[a].source) {
// If not, give up and try a different match.
continue hookSearch;
}
}
return i;
}
}
return -1;
}
function findCommonAncestorIndex(rootStack, hookStack) {
let rootIndex = findSharedIndex(
hookStack,
rootStack,
mostLikelyAncestorIndex,
);
if (rootIndex !== -1) {
return rootIndex;
}
// If the most likely one wasn't a hit, try any other frame to see if it is shared.
// If that takes more than 5 frames, something probably went wrong.
for (let i = 0; i < rootStack.length && i < 5; i++) {
rootIndex = findSharedIndex(hookStack, rootStack, i);
if (rootIndex !== -1) {
mostLikelyAncestorIndex = i;
return rootIndex;
}
}
return -1;
}
function isReactWrapper(functionName, primitiveName) {
if (!functionName) {
return false;
}
let expectedPrimitiveName = 'use' + primitiveName;
if (functionName.length < expectedPrimitiveName.length) {
return false;
}
return (
functionName.lastIndexOf(expectedPrimitiveName) ===
functionName.length - expectedPrimitiveName.length
);
}
function findPrimitiveIndex(hookStack, hook) {
let stackCache = getPrimitiveStackCache();
let primitiveStack = stackCache.get(hook.primitive);
if (primitiveStack === undefined) {
return -1;
}
for (let i = 0; i < primitiveStack.length && i < hookStack.length; i++) {
if (primitiveStack[i].source !== hookStack[i].source) {
// If the next two frames are functions called `useX` then we assume that they're part of the
// wrappers that the React packager or other packages adds around the dispatcher.
if (
i < hookStack.length - 1 &&
isReactWrapper(hookStack[i].functionName, hook.primitive)
) {
i++;
}
if (
i < hookStack.length - 1 &&
isReactWrapper(hookStack[i].functionName, hook.primitive)
) {
i++;
}
return i;
}
}
return -1;
}
function parseTrimmedStack(rootStack, hook) {
// Get the stack trace between the primitive hook function and
// the root function call. I.e. the stack frames of custom hooks.
let hookStack = ErrorStackParser.parse(hook.stackError);
let rootIndex = findCommonAncestorIndex(rootStack, hookStack);
let primitiveIndex = findPrimitiveIndex(hookStack, hook);
if (
rootIndex === -1 ||
primitiveIndex === -1 ||
rootIndex - primitiveIndex < 2
) {
// Something went wrong. Give up.
return null;
}
return hookStack.slice(primitiveIndex, rootIndex - 1);
}
function parseCustomHookName(functionName: void | string): string {
if (!functionName) {
return '';
}
let startIndex = functionName.lastIndexOf('.');
if (startIndex === -1) {
startIndex = 0;
}
if (functionName.substr(startIndex, 3) === 'use') {
startIndex += 3;
}
return functionName.substr(startIndex);
}
function buildTree(rootStack, readHookLog): HooksTree {
let rootChildren = [];
let prevStack = null;
let levelChildren = rootChildren;
let stackOfChildren = [];
for (let i = 0; i < readHookLog.length; i++) {
let hook = readHookLog[i];
let stack = parseTrimmedStack(rootStack, hook);
if (stack !== null) {
// Note: The indices 0 <= n < length-1 will contain the names.
// The indices 1 <= n < length will contain the source locations.
// That's why we get the name from n - 1 and don't check the source
// of index 0.
let commonSteps = 0;
if (prevStack !== null) {
// Compare the current level's stack to the new stack.
while (commonSteps < stack.length && commonSteps < prevStack.length) {
let stackSource = stack[stack.length - commonSteps - 1].source;
let prevSource = prevStack[prevStack.length - commonSteps - 1].source;
if (stackSource !== prevSource) {
break;
}
commonSteps++;
}
// Pop back the stack as many steps as were not common.
for (let j = prevStack.length - 1; j > commonSteps; j--) {
levelChildren = stackOfChildren.pop();
}
}
// The remaining part of the new stack are custom hooks. Push them
// to the tree.
for (let j = stack.length - commonSteps - 1; j >= 1; j--) {
let children = [];
levelChildren.push({
name: parseCustomHookName(stack[j - 1].functionName),
value: undefined, // TODO: Support custom inspectable values.
subHooks: children,
});
stackOfChildren.push(levelChildren);
levelChildren = children;
}
prevStack = stack;
}
levelChildren.push({
name: hook.primitive,
value: hook.value,
subHooks: [],
});
}
return rootChildren;
}
export function inspectHooks<Props>(
renderFunction: Props => React$Node,
props: Props,
): HooksTree {
let previousDispatcher = ReactCurrentOwner.currentDispatcher;
let readHookLog;
ReactCurrentOwner.currentDispatcher = Dispatcher;
let ancestorStackError;
try {
ancestorStackError = new Error();
renderFunction(props);
} finally {
readHookLog = hookLog;
hookLog = [];
ReactCurrentOwner.currentDispatcher = previousDispatcher;
}
let rootStack = ErrorStackParser.parse(ancestorStackError);
return buildTree(rootStack, readHookLog);
}
function setupContexts(contextMap: Map<ReactContext<any>, any>, fiber: Fiber) {
let current = fiber;
while (current) {
if (current.tag === ContextProvider) {
const providerType: ReactProviderType<any> = current.type;
const context: ReactContext<any> = providerType._context;
if (!contextMap.has(context)) {
// Store the current value that we're going to restore later.
contextMap.set(context, context._currentValue);
// Set the inner most provider value on the context.
context._currentValue = current.memoizedProps.value;
}
}
current = current.return;
}
}
function restoreContexts(contextMap: Map<ReactContext<any>, any>) {
contextMap.forEach((value, context) => (context._currentValue = value));
}
function inspectHooksOfForwardRef<Props, Ref>(
renderFunction: (Props, Ref) => React$Node,
props: Props,
ref: Ref,
): HooksTree {
let previousDispatcher = ReactCurrentOwner.currentDispatcher;
let readHookLog;
ReactCurrentOwner.currentDispatcher = Dispatcher;
let ancestorStackError;
try {
ancestorStackError = new Error();
renderFunction(props, ref);
} finally {
readHookLog = hookLog;
hookLog = [];
ReactCurrentOwner.currentDispatcher = previousDispatcher;
}
let rootStack = ErrorStackParser.parse(ancestorStackError);
return buildTree(rootStack, readHookLog);
}
function resolveDefaultProps(Component, baseProps) {
if (Component && Component.defaultProps) {
// Resolve default props. Taken from ReactElement
const props = Object.assign({}, baseProps);
const defaultProps = Component.defaultProps;
for (let propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
return props;
}
return baseProps;
}
export function inspectHooksOfFiber(fiber: Fiber) {
if (
fiber.tag !== FunctionComponent &&
fiber.tag !== SimpleMemoComponent &&
fiber.tag !== ForwardRef
) {
throw new Error(
'Unknown Fiber. Needs to be a function component to inspect hooks.',
);
}
// Warm up the cache so that it doesn't consume the currentHook.
getPrimitiveStackCache();
let type = fiber.type;
let props = fiber.memoizedProps;
if (type !== fiber.elementType) {
props = resolveDefaultProps(type, props);
}
// Set up the current hook so that we can step through and read the
// current state from them.
currentHook = (fiber.memoizedState: Hook);
let contextMap = new Map();
try {
setupContexts(contextMap, fiber);
if (fiber.tag === ForwardRef) {
return inspectHooksOfForwardRef(type.render, props, fiber.ref);
}
return inspectHooks(type, props);
} finally {
currentHook = null;
restoreContexts(contextMap);
}
}

View File

@ -0,0 +1,12 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import {inspectHooks, inspectHooksOfFiber} from './ReactDebugHooks';
export {inspectHooks, inspectHooksOfFiber};

View File

@ -0,0 +1,219 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
* @jest-environment node
*/
'use strict';
let React;
let ReactDebugTools;
describe('ReactHooksInspection', () => {
beforeEach(() => {
jest.resetModules();
let ReactFeatureFlags = require('shared/ReactFeatureFlags');
// TODO: Switch this test to non-internal once the flag is on by default.
ReactFeatureFlags.enableHooks = true;
React = require('react');
ReactDebugTools = require('react-debug-tools');
});
it('should inspect a simple useState hook', () => {
function Foo(props) {
let [state] = React.useState('hello world');
return <div>{state}</div>;
}
let tree = ReactDebugTools.inspectHooks(Foo, {});
expect(tree).toEqual([
{
name: 'State',
value: 'hello world',
subHooks: [],
},
]);
});
it('should inspect a simple custom hook', () => {
function useCustom(value) {
let [state] = React.useState(value);
return state;
}
function Foo(props) {
let value = useCustom('hello world');
return <div>{value}</div>;
}
let tree = ReactDebugTools.inspectHooks(Foo, {});
expect(tree).toEqual([
{
name: 'Custom',
value: undefined,
subHooks: [
{
name: 'State',
value: 'hello world',
subHooks: [],
},
],
},
]);
});
it('should inspect a tree of multiple hooks', () => {
function effect() {}
function useCustom(value) {
let [state] = React.useState(value);
React.useEffect(effect);
return state;
}
function Foo(props) {
let value1 = useCustom('hello');
let value2 = useCustom('world');
return (
<div>
{value1} {value2}
</div>
);
}
let tree = ReactDebugTools.inspectHooks(Foo, {});
expect(tree).toEqual([
{
name: 'Custom',
value: undefined,
subHooks: [
{
name: 'State',
subHooks: [],
value: 'hello',
},
{
name: 'Effect',
subHooks: [],
value: effect,
},
],
},
{
name: 'Custom',
value: undefined,
subHooks: [
{
name: 'State',
value: 'world',
subHooks: [],
},
{
name: 'Effect',
value: effect,
subHooks: [],
},
],
},
]);
});
it('should inspect a tree of multiple levels of hooks', () => {
function effect() {}
function useCustom(value) {
let [state] = React.useReducer((s, a) => s, value);
React.useEffect(effect);
return state;
}
function useBar(value) {
let result = useCustom(value);
React.useLayoutEffect(effect);
return result;
}
function useBaz(value) {
React.useMutationEffect(effect);
let result = useCustom(value);
return result;
}
function Foo(props) {
let value1 = useBar('hello');
let value2 = useBaz('world');
return (
<div>
{value1} {value2}
</div>
);
}
let tree = ReactDebugTools.inspectHooks(Foo, {});
expect(tree).toEqual([
{
name: 'Bar',
value: undefined,
subHooks: [
{
name: 'Custom',
value: undefined,
subHooks: [
{
name: 'Reducer',
value: 'hello',
subHooks: [],
},
{
name: 'Effect',
value: effect,
subHooks: [],
},
],
},
{
name: 'LayoutEffect',
value: effect,
subHooks: [],
},
],
},
{
name: 'Baz',
value: undefined,
subHooks: [
{
name: 'MutationEffect',
value: effect,
subHooks: [],
},
{
name: 'Custom',
subHooks: [
{
name: 'Reducer',
subHooks: [],
value: 'world',
},
{
name: 'Effect',
subHooks: [],
value: effect,
},
],
value: undefined,
},
],
},
]);
});
it('should inspect the default value using the useContext hook', () => {
let MyContext = React.createContext('default');
function Foo(props) {
let value = React.useContext(MyContext);
return <div>{value}</div>;
}
let tree = ReactDebugTools.inspectHooks(Foo, {});
expect(tree).toEqual([
{
name: 'Context',
value: 'default',
subHooks: [],
},
]);
});
});

View File

@ -0,0 +1,247 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
* @jest-environment node
*/
'use strict';
let React;
let ReactTestRenderer;
let ReactDebugTools;
describe('ReactHooksInspectionIntergration', () => {
beforeEach(() => {
jest.resetModules();
let ReactFeatureFlags = require('shared/ReactFeatureFlags');
// TODO: Switch this test to non-internal once the flag is on by default.
ReactFeatureFlags.enableHooks = true;
React = require('react');
ReactTestRenderer = require('react-test-renderer');
ReactDebugTools = require('react-debug-tools');
});
it('should inspect the current state of useState hooks', () => {
let useState = React.useState;
function Foo(props) {
let [state1, setState1] = useState('hello');
let [state2, setState2] = useState('world');
return (
<div onMouseDown={setState1} onMouseUp={setState2}>
{state1} {state2}
</div>
);
}
let renderer = ReactTestRenderer.create(<Foo prop="prop" />);
let childFiber = renderer.root.findByType(Foo)._currentFiber();
let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
expect(tree).toEqual([
{name: 'State', value: 'hello', subHooks: []},
{name: 'State', value: 'world', subHooks: []},
]);
let {
onMouseDown: setStateA,
onMouseUp: setStateB,
} = renderer.root.findByType('div').props;
setStateA('Hi');
childFiber = renderer.root.findByType(Foo)._currentFiber();
tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
expect(tree).toEqual([
{name: 'State', value: 'Hi', subHooks: []},
{name: 'State', value: 'world', subHooks: []},
]);
setStateB('world!');
childFiber = renderer.root.findByType(Foo)._currentFiber();
tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
expect(tree).toEqual([
{name: 'State', value: 'Hi', subHooks: []},
{name: 'State', value: 'world!', subHooks: []},
]);
});
it('should inspect the current state of all stateful hooks', () => {
let outsideRef = React.createRef();
function effect() {}
function Foo(props) {
let [state1, setState] = React.useState('a');
let [state2, dispatch] = React.useReducer((s, a) => a.value, 'b');
let ref = React.useRef('c');
React.useMutationEffect(effect);
React.useLayoutEffect(effect);
React.useEffect(effect);
React.useImperativeMethods(
outsideRef,
() => {
// Return a function so that jest treats them as non-equal.
return function Instance() {};
},
[],
);
React.useMemo(() => state1 + state2, [state1]);
function update() {
setState('A');
dispatch({value: 'B'});
ref.current = 'C';
}
let memoizedUpdate = React.useCallback(update, []);
return (
<div onClick={memoizedUpdate}>
{state1} {state2}
</div>
);
}
let renderer = ReactTestRenderer.create(<Foo prop="prop" />);
let childFiber = renderer.root.findByType(Foo)._currentFiber();
let {onClick: updateStates} = renderer.root.findByType('div').props;
let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
expect(tree).toEqual([
{name: 'State', value: 'a', subHooks: []},
{name: 'Reducer', value: 'b', subHooks: []},
{name: 'Ref', value: 'c', subHooks: []},
{name: 'MutationEffect', value: effect, subHooks: []},
{name: 'LayoutEffect', value: effect, subHooks: []},
{name: 'Effect', value: effect, subHooks: []},
{name: 'ImperativeMethods', value: outsideRef.current, subHooks: []},
{name: 'Memo', value: 'ab', subHooks: []},
{name: 'Callback', value: updateStates, subHooks: []},
]);
updateStates();
childFiber = renderer.root.findByType(Foo)._currentFiber();
tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
expect(tree).toEqual([
{name: 'State', value: 'A', subHooks: []},
{name: 'Reducer', value: 'B', subHooks: []},
{name: 'Ref', value: 'C', subHooks: []},
{name: 'MutationEffect', value: effect, subHooks: []},
{name: 'LayoutEffect', value: effect, subHooks: []},
{name: 'Effect', value: effect, subHooks: []},
{name: 'ImperativeMethods', value: outsideRef.current, subHooks: []},
{name: 'Memo', value: 'Ab', subHooks: []},
{name: 'Callback', value: updateStates, subHooks: []},
]);
});
it('should inspect the value of the current provider in useContext', () => {
let MyContext = React.createContext('default');
function Foo(props) {
let value = React.useContext(MyContext);
return <div>{value}</div>;
}
let renderer = ReactTestRenderer.create(
<MyContext.Provider value="contextual">
<Foo prop="prop" />
</MyContext.Provider>,
);
let childFiber = renderer.root.findByType(Foo)._currentFiber();
let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
expect(tree).toEqual([
{
name: 'Context',
value: 'contextual',
subHooks: [],
},
]);
});
it('should inspect forwardRef', () => {
let obj = function() {};
let Foo = React.forwardRef(function(props, ref) {
React.useImperativeMethods(ref, () => obj);
return <div />;
});
let ref = React.createRef();
let renderer = ReactTestRenderer.create(<Foo ref={ref} />);
let childFiber = renderer.root.findByType(Foo)._currentFiber();
let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
expect(tree).toEqual([
{name: 'ImperativeMethods', value: obj, subHooks: []},
]);
});
it('should inspect memo', () => {
function InnerFoo(props) {
let [value] = React.useState('hello');
return <div>{value}</div>;
}
let Foo = React.memo(InnerFoo);
let renderer = ReactTestRenderer.create(<Foo />);
// TODO: Test renderer findByType is broken for memo. Have to search for the inner.
let childFiber = renderer.root.findByType(InnerFoo)._currentFiber();
let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
expect(tree).toEqual([{name: 'State', value: 'hello', subHooks: []}]);
});
it('should inspect custom hooks', () => {
function useCustom() {
let [value] = React.useState('hello');
return value;
}
function Foo(props) {
let value = useCustom();
return <div>{value}</div>;
}
let renderer = ReactTestRenderer.create(<Foo />);
let childFiber = renderer.root.findByType(Foo)._currentFiber();
let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
expect(tree).toEqual([
{
name: 'Custom',
value: undefined,
subHooks: [{name: 'State', value: 'hello', subHooks: []}],
},
]);
});
it('should support defaultProps and lazy', async () => {
let Suspense = React.Suspense;
function Foo(props) {
let [value] = React.useState(props.defaultValue.substr(0, 3));
return <div>{value}</div>;
}
Foo.defaultProps = {
defaultValue: 'default',
};
async function fakeImport(result) {
return {default: result};
}
let LazyFoo = React.lazy(() => fakeImport(Foo));
let renderer = ReactTestRenderer.create(
<Suspense fallback="Loading...">
<LazyFoo />
</Suspense>,
);
await LazyFoo;
let childFiber = renderer.root._currentFiber();
let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
expect(tree).toEqual([{name: 'State', value: 'def', subHooks: []}]);
});
});

View File

@ -49,7 +49,7 @@ type UpdateQueue<A> = {
dispatch: any,
};
type Hook = {
export type Hook = {
memoizedState: any,
baseState: any,

View File

@ -211,6 +211,7 @@ const validWrapperTypes = new Set([
HostComponent,
ForwardRef,
MemoComponent,
SimpleMemoComponent,
// Normally skipped, but used when there's more than one root child.
HostRoot,
]);

View File

@ -363,6 +363,16 @@ const bundles = [
externals: [],
},
/******* React Debug Tools *******/
{
label: 'react-debug-tools',
bundleTypes: [NODE_DEV, NODE_PROD],
moduleType: ISOMORPHIC,
entry: 'react-debug-tools',
global: 'ReactDebugTools',
externals: [],
},
/******* React Cache (experimental) *******/
{
label: 'react-cache',

View File

@ -4,29 +4,29 @@
"filename": "react.development.js",
"bundleType": "UMD_DEV",
"packageName": "react",
"size": 95936,
"gzip": 25258
"size": 98406,
"gzip": 25783
},
{
"filename": "react.production.min.js",
"bundleType": "UMD_PROD",
"packageName": "react",
"size": 11933,
"gzip": 4716
"size": 11771,
"gzip": 4678
},
{
"filename": "react.development.js",
"bundleType": "NODE_DEV",
"packageName": "react",
"size": 57749,
"gzip": 15548
"size": 61674,
"gzip": 16606
},
{
"filename": "react.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react",
"size": 6215,
"gzip": 2648
"size": 6223,
"gzip": 2655
},
{
"filename": "React-dev.js",
@ -46,29 +46,29 @@
"filename": "react-dom.development.js",
"bundleType": "UMD_DEV",
"packageName": "react-dom",
"size": 697227,
"gzip": 161844
"size": 720847,
"gzip": 166955
},
{
"filename": "react-dom.production.min.js",
"bundleType": "UMD_PROD",
"packageName": "react-dom",
"size": 100367,
"gzip": 32791
"size": 105329,
"gzip": 34551
},
{
"filename": "react-dom.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-dom",
"size": 692458,
"gzip": 160422
"size": 716045,
"gzip": 165551
},
{
"filename": "react-dom.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-dom",
"size": 100389,
"gzip": 32291
"size": 105421,
"gzip": 34088
},
{
"filename": "ReactDOM-dev.js",
@ -88,8 +88,8 @@
"filename": "react-dom-test-utils.development.js",
"bundleType": "UMD_DEV",
"packageName": "react-dom",
"size": 46915,
"gzip": 12731
"size": 46926,
"gzip": 12739
},
{
"filename": "react-dom-test-utils.production.min.js",
@ -102,8 +102,8 @@
"filename": "react-dom-test-utils.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-dom",
"size": 46629,
"gzip": 12668
"size": 46640,
"gzip": 12676
},
{
"filename": "react-dom-test-utils.production.min.js",
@ -165,29 +165,29 @@
"filename": "react-dom-server.browser.development.js",
"bundleType": "UMD_DEV",
"packageName": "react-dom",
"size": 110808,
"gzip": 29290
"size": 120946,
"gzip": 31984
},
{
"filename": "react-dom-server.browser.production.min.js",
"bundleType": "UMD_PROD",
"packageName": "react-dom",
"size": 16261,
"gzip": 6183
"size": 18147,
"gzip": 6947
},
{
"filename": "react-dom-server.browser.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-dom",
"size": 106846,
"gzip": 28277
"size": 116984,
"gzip": 31008
},
{
"filename": "react-dom-server.browser.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-dom",
"size": 16156,
"gzip": 6171
"size": 18045,
"gzip": 6934
},
{
"filename": "ReactDOMServer-dev.js",
@ -207,43 +207,43 @@
"filename": "react-dom-server.node.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-dom",
"size": 108814,
"gzip": 28843
"size": 118952,
"gzip": 31557
},
{
"filename": "react-dom-server.node.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-dom",
"size": 16981,
"gzip": 6481
"size": 18870,
"gzip": 7243
},
{
"filename": "react-art.development.js",
"bundleType": "UMD_DEV",
"packageName": "react-art",
"size": 479568,
"gzip": 106369
"size": 502853,
"gzip": 111403
},
{
"filename": "react-art.production.min.js",
"bundleType": "UMD_PROD",
"packageName": "react-art",
"size": 92149,
"gzip": 28279
"size": 96973,
"gzip": 30018
},
{
"filename": "react-art.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-art",
"size": 409729,
"gzip": 88882
"size": 433008,
"gzip": 93974
},
{
"filename": "react-art.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-art",
"size": 56279,
"gzip": 17346
"size": 61174,
"gzip": 19077
},
{
"filename": "ReactART-dev.js",
@ -291,29 +291,29 @@
"filename": "react-test-renderer.development.js",
"bundleType": "UMD_DEV",
"packageName": "react-test-renderer",
"size": 422767,
"gzip": 91691
"size": 445955,
"gzip": 96705
},
{
"filename": "react-test-renderer.production.min.js",
"bundleType": "UMD_PROD",
"packageName": "react-test-renderer",
"size": 57493,
"gzip": 17706
"size": 62392,
"gzip": 19461
},
{
"filename": "react-test-renderer.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-test-renderer",
"size": 418235,
"gzip": 90567
"size": 441056,
"gzip": 95534
},
{
"filename": "react-test-renderer.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-test-renderer",
"size": 57208,
"gzip": 17513
"size": 62067,
"gzip": 19233
},
{
"filename": "ReactTestRenderer-dev.js",
@ -326,21 +326,21 @@
"filename": "react-test-renderer-shallow.development.js",
"bundleType": "UMD_DEV",
"packageName": "react-test-renderer",
"size": 27270,
"gzip": 7324
"size": 27383,
"gzip": 7340
},
{
"filename": "react-test-renderer-shallow.production.min.js",
"bundleType": "UMD_PROD",
"packageName": "react-test-renderer",
"size": 7394,
"gzip": 2412
"size": 7442,
"gzip": 2425
},
{
"filename": "react-test-renderer-shallow.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-test-renderer",
"size": 21638,
"size": 21639,
"gzip": 5871
},
{
@ -361,50 +361,50 @@
"filename": "react-noop-renderer.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-noop-renderer",
"size": 28548,
"gzip": 6219
"size": 28829,
"gzip": 6286
},
{
"filename": "react-noop-renderer.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-noop-renderer",
"size": 10802,
"gzip": 3587
"size": 10889,
"gzip": 3632
},
{
"filename": "react-reconciler.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-reconciler",
"size": 407389,
"gzip": 87287
"size": 430802,
"gzip": 92370
},
{
"filename": "react-reconciler.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-reconciler",
"size": 57356,
"gzip": 17175
"size": 62418,
"gzip": 18868
},
{
"filename": "react-reconciler-persistent.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-reconciler",
"size": 405959,
"gzip": 86717
"size": 429212,
"gzip": 91734
},
{
"filename": "react-reconciler-persistent.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-reconciler",
"size": 57367,
"gzip": 17180
"size": 62429,
"gzip": 18874
},
{
"filename": "react-reconciler-reflection.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-reconciler",
"size": 16735,
"gzip": 5071
"size": 16747,
"gzip": 5081
},
{
"filename": "react-reconciler-reflection.production.min.js",
@ -431,29 +431,29 @@
"filename": "react-is.development.js",
"bundleType": "UMD_DEV",
"packageName": "react-is",
"size": 7539,
"gzip": 2364
"size": 7691,
"gzip": 2393
},
{
"filename": "react-is.production.min.js",
"bundleType": "UMD_PROD",
"packageName": "react-is",
"size": 2113,
"gzip": 836
"size": 2171,
"gzip": 854
},
{
"filename": "react-is.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-is",
"size": 7350,
"gzip": 2313
"size": 7502,
"gzip": 2344
},
{
"filename": "react-is.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-is",
"size": 2074,
"gzip": 775
"size": 2132,
"gzip": 793
},
{
"filename": "ReactIs-dev.js",
@ -501,36 +501,36 @@
"filename": "React-dev.js",
"bundleType": "FB_WWW_DEV",
"packageName": "react",
"size": 54009,
"gzip": 14673
"size": 58044,
"gzip": 15664
},
{
"filename": "React-prod.js",
"bundleType": "FB_WWW_PROD",
"packageName": "react",
"size": 14256,
"size": 14026,
"gzip": 3899
},
{
"filename": "ReactDOM-dev.js",
"bundleType": "FB_WWW_DEV",
"packageName": "react-dom",
"size": 711278,
"gzip": 161142
"size": 734797,
"gzip": 166398
},
{
"filename": "ReactDOM-prod.js",
"bundleType": "FB_WWW_PROD",
"packageName": "react-dom",
"size": 303046,
"gzip": 55626
"size": 318299,
"gzip": 58930
},
{
"filename": "ReactTestUtils-dev.js",
"bundleType": "FB_WWW_DEV",
"packageName": "react-dom",
"size": 42309,
"gzip": 11422
"size": 42319,
"gzip": 11429
},
{
"filename": "ReactDOMUnstableNativeDependencies-dev.js",
@ -550,113 +550,113 @@
"filename": "ReactDOMServer-dev.js",
"bundleType": "FB_WWW_DEV",
"packageName": "react-dom",
"size": 106413,
"gzip": 27558
"size": 116990,
"gzip": 30470
},
{
"filename": "ReactDOMServer-prod.js",
"bundleType": "FB_WWW_PROD",
"packageName": "react-dom",
"size": 35825,
"gzip": 8548
"size": 41749,
"gzip": 9884
},
{
"filename": "ReactART-dev.js",
"bundleType": "FB_WWW_DEV",
"packageName": "react-art",
"size": 414596,
"gzip": 87310
"size": 437769,
"gzip": 92475
},
{
"filename": "ReactART-prod.js",
"bundleType": "FB_WWW_PROD",
"packageName": "react-art",
"size": 173284,
"gzip": 29264
"size": 188325,
"gzip": 32413
},
{
"filename": "ReactNativeRenderer-dev.js",
"bundleType": "RN_FB_DEV",
"packageName": "react-native-renderer",
"size": 544253,
"gzip": 118996
"size": 567081,
"gzip": 124053
},
{
"filename": "ReactNativeRenderer-prod.js",
"bundleType": "RN_FB_PROD",
"packageName": "react-native-renderer",
"size": 229349,
"gzip": 39944
"size": 244223,
"gzip": 43036
},
{
"filename": "ReactNativeRenderer-dev.js",
"bundleType": "RN_OSS_DEV",
"packageName": "react-native-renderer",
"size": 543957,
"gzip": 118920
"size": 566746,
"gzip": 123945
},
{
"filename": "ReactNativeRenderer-prod.js",
"bundleType": "RN_OSS_PROD",
"packageName": "react-native-renderer",
"size": 229363,
"gzip": 39944
"size": 244237,
"gzip": 43035
},
{
"filename": "ReactFabric-dev.js",
"bundleType": "RN_FB_DEV",
"packageName": "react-native-renderer",
"size": 534243,
"gzip": 116534
"size": 557032,
"gzip": 121529
},
{
"filename": "ReactFabric-prod.js",
"bundleType": "RN_FB_PROD",
"packageName": "react-native-renderer",
"size": 223762,
"gzip": 38608
"size": 238983,
"gzip": 41707
},
{
"filename": "ReactFabric-dev.js",
"bundleType": "RN_OSS_DEV",
"packageName": "react-native-renderer",
"size": 534278,
"gzip": 116548
"size": 557067,
"gzip": 121541
},
{
"filename": "ReactFabric-prod.js",
"bundleType": "RN_OSS_PROD",
"packageName": "react-native-renderer",
"size": 223798,
"gzip": 38624
"size": 239019,
"gzip": 41721
},
{
"filename": "ReactTestRenderer-dev.js",
"bundleType": "FB_WWW_DEV",
"packageName": "react-test-renderer",
"size": 423481,
"gzip": 89361
"size": 446010,
"gzip": 94386
},
{
"filename": "ReactShallowRenderer-dev.js",
"bundleType": "FB_WWW_DEV",
"packageName": "react-test-renderer",
"size": 18563,
"gzip": 4857
"size": 18564,
"gzip": 4859
},
{
"filename": "ReactIs-dev.js",
"bundleType": "FB_WWW_DEV",
"packageName": "react-is",
"size": 5721,
"gzip": 1588
"size": 5873,
"gzip": 1621
},
{
"filename": "ReactIs-prod.js",
"bundleType": "FB_WWW_PROD",
"packageName": "react-is",
"size": 4227,
"gzip": 1105
"size": 4382,
"gzip": 1135
},
{
"filename": "scheduler.development.js",
@ -676,15 +676,15 @@
"filename": "scheduler.development.js",
"bundleType": "NODE_DEV",
"packageName": "scheduler",
"size": 22710,
"gzip": 6136
"size": 22073,
"gzip": 5976
},
{
"filename": "scheduler.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "scheduler",
"size": 4907,
"gzip": 1905
"size": 4755,
"gzip": 1865
},
{
"filename": "SimpleCacheProvider-dev.js",
@ -704,50 +704,50 @@
"filename": "react-noop-renderer-persistent.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-noop-renderer",
"size": 28667,
"gzip": 6232
"size": 28948,
"gzip": 6299
},
{
"filename": "react-noop-renderer-persistent.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-noop-renderer",
"size": 10824,
"gzip": 3593
"size": 10911,
"gzip": 3639
},
{
"filename": "react-dom.profiling.min.js",
"bundleType": "NODE_PROFILING",
"packageName": "react-dom",
"size": 102854,
"gzip": 32671
"size": 107853,
"gzip": 34402
},
{
"filename": "ReactNativeRenderer-profiling.js",
"bundleType": "RN_OSS_PROFILING",
"packageName": "react-native-renderer",
"size": 235178,
"gzip": 41307
"size": 250067,
"gzip": 44259
},
{
"filename": "ReactFabric-profiling.js",
"bundleType": "RN_OSS_PROFILING",
"packageName": "react-native-renderer",
"size": 228734,
"gzip": 39942
"size": 243670,
"gzip": 43000
},
{
"filename": "Scheduler-dev.js",
"bundleType": "FB_WWW_DEV",
"packageName": "scheduler",
"size": 22987,
"gzip": 6180
"size": 22314,
"gzip": 6027
},
{
"filename": "Scheduler-prod.js",
"bundleType": "FB_WWW_PROD",
"packageName": "scheduler",
"size": 13870,
"gzip": 2990
"size": 13375,
"gzip": 2927
},
{
"filename": "react.profiling.min.js",
@ -760,50 +760,50 @@
"filename": "React-profiling.js",
"bundleType": "FB_WWW_PROFILING",
"packageName": "react",
"size": 14256,
"size": 14026,
"gzip": 3899
},
{
"filename": "ReactDOM-profiling.js",
"bundleType": "FB_WWW_PROFILING",
"packageName": "react-dom",
"size": 307635,
"gzip": 56727
"size": 322659,
"gzip": 59833
},
{
"filename": "ReactNativeRenderer-profiling.js",
"bundleType": "RN_FB_PROFILING",
"packageName": "react-native-renderer",
"size": 235159,
"gzip": 41309
"size": 250048,
"gzip": 44263
},
{
"filename": "ReactFabric-profiling.js",
"bundleType": "RN_FB_PROFILING",
"packageName": "react-native-renderer",
"size": 228693,
"gzip": 39926
"size": 243629,
"gzip": 42984
},
{
"filename": "react.profiling.min.js",
"bundleType": "UMD_PROFILING",
"packageName": "react",
"size": 14140,
"gzip": 5250
"size": 13977,
"gzip": 5211
},
{
"filename": "react-dom.profiling.min.js",
"bundleType": "UMD_PROFILING",
"packageName": "react-dom",
"size": 102786,
"gzip": 33199
"size": 107720,
"gzip": 34916
},
{
"filename": "scheduler-tracing.development.js",
"bundleType": "NODE_DEV",
"packageName": "scheduler",
"size": 10319,
"gzip": 2326
"size": 10480,
"gzip": 2403
},
{
"filename": "scheduler-tracing.production.min.js",
@ -823,8 +823,8 @@
"filename": "SchedulerTracing-dev.js",
"bundleType": "FB_WWW_DEV",
"packageName": "scheduler",
"size": 10105,
"gzip": 2117
"size": 10467,
"gzip": 2295
},
{
"filename": "SchedulerTracing-prod.js",
@ -909,6 +909,34 @@
"packageName": "jest-react",
"size": 5201,
"gzip": 1623
},
{
"filename": "react-debug-tools.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-debug-tools",
"size": 14080,
"gzip": 4317
},
{
"filename": "react-debug-tools.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-debug-tools",
"size": 4112,
"gzip": 1643
},
{
"filename": "eslint-plugin-react-hooks.development.js",
"bundleType": "NODE_DEV",
"packageName": "eslint-plugin-react-hooks",
"size": 25596,
"gzip": 5886
},
{
"filename": "eslint-plugin-react-hooks.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "eslint-plugin-react-hooks",
"size": 4943,
"gzip": 1815
}
]
}

View File

@ -1584,6 +1584,13 @@ error-ex@^1.2.0:
dependencies:
is-arrayish "^0.2.1"
error-stack-parser@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.0.2.tgz#4ae8dbaa2bf90a8b450707b9149dcabca135520d"
integrity sha512-E1fPutRDdIj/hohG0UpT5mayXNCxXP9d+snxFsPU9X0XgccOumKraa3juDMwTUyi7+Bu5+mCGagjg4IYeNbOdw==
dependencies:
stackframe "^1.0.4"
es-abstract@^1.5.1:
version "1.12.0"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.12.0.tgz#9dbbdd27c6856f0001421ca18782d786bf8a6165"
@ -4638,6 +4645,11 @@ stack-utils@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.1.tgz#d4f33ab54e8e38778b0ca5cfd3b3afb12db68620"
stackframe@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.0.4.tgz#357b24a992f9427cba6b545d96a14ed2cbca187b"
integrity sha512-to7oADIniaYwS3MhtCa/sQhrxidCCQiF/qp4/m5iN3ipf0Y7Xlri0f6eG29r08aL7JYl8n32AF3Q5GYBZ7K8vw==
static-extend@^0.1.1:
version "0.1.2"
resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"