Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Debug Tools Package for Introspection of Hooks #14085

Merged
merged 9 commits into from
Nov 5, 2018

Conversation

sebmarkbage
Copy link
Collaborator

@sebmarkbage sebmarkbage commented Nov 3, 2018

Adds introspection of primitive and custom hooks in a single function component. If you feed it a standalone component it just gives you the initial/default values as the current value of each primitive. If you pass it a Fiber, it gives you the current values by reading the state from the Fiber.

Ideally we want this in DevTools but it requires so much implementation details that I don't think we'll want to maintain this with multi-version support. We'll probably have to move to snapshot versions of this package in DevTools, or just snapshoting all of DevTools. It's probably useful debug info for others too. So I decided to just make this a new separate package.

The algorithm goes something like this:

  1. If running in Fiber mode, set up contexts of all parent providers and set the current hook to the first one of the Fiber. If running in standalone mode, skip this step.
  2. Compute the stack trace from the root of the inspection function.
  3. Set up a custom Dispatcher.
  4. Compute the stack traces to all primitive hooks, using the custom Dispatcher, when used outside a component.
  5. Execute the render function of the inspected component. This will call all hooks in order since they have to be unconditional. It's safe because the render function as no side-effects on its own.
  6. Inside each primitive hook in the custom Dispatcher, log each primitive, it's current stateful value, and the stack trace at the time it was invoked. T
    his will give us a list of all primitive hooks and their stacks.
  7. For each primitive hook in the log, process the stack trace by removing any shared ancestors with the root of the inspection function. This ensures that the bottom of the stack is the render function. Also, for that particular primitive, remove the top of the stack that is shared with the stack when that primitive is called stand alone from a function component. This removes the call to the primitive itself. If the top most function call is called "useState" or whatever the primitive is called, remove that too since it's the React package wrapper around the dispatcher. The result should be a stack of only the custom hooks.
  8. Loop through the list of primitive hooks, use the stack of custom hooks to form a tree structure. Shared parent paths means that they go into the same subnode.
  9. ...
  10. Profit.

Currently custom hooks doesn't have a "value" associated with them so you can only introspect the current values of primitives. In a follow up, we can add a DEV only API to share more information from custom hooks with the devtools. E.g. useInspect(value)

@sizebot
Copy link

sizebot commented Nov 3, 2018

Details of bundled changes.

Comparing: 8eca0ef...7006c4b

scheduler

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
scheduler.development.js n/a n/a 0 B 19.17 KB 0 B 5.74 KB UMD_DEV
scheduler.production.min.js n/a n/a 0 B 3.16 KB 0 B 1.53 KB UMD_PROD

react-debug-tools

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-debug-tools.development.js n/a n/a 0 B 16.33 KB 0 B 4.85 KB NODE_DEV
react-debug-tools.production.min.js n/a n/a 0 B 5.28 KB 0 B 2.14 KB NODE_PROD

Generated by 🚫 dangerJS

Copy link
Collaborator

@sophiebits sophiebits left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice


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.**
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

, and it

* LICENSE file in the root directory of this source tree.
*/

// This entry point is intentionally not typed. It exists only for third-party
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copy pasta?

}

function parseCustomHookName(functionName: string): string {
let startIndex = functionName.indexOf('.');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lastIndexOf?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Don’t know if any conditions report multiple levels but there is at least one.

}

export function inspectHooksOfFiber(fiber: Fiber) {
if (fiber.tag !== FunctionComponent && fiber.tag !== SimpleMemoComponent) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can’t ForwardRef have hooks?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useImperativeMethods specifically only works with ForwardRef too. I opened a related issue at facebook/react-devtools#1213.

}

function isReactWrapper(functionName, primitiveName) {
let expectedPrimitiveName = 'use' + primitiveName;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this to handle dots? you could also do

let basename = functionName.split('.').pop();
return basename === primitiveName;

kinda hard to tell what names this is trying to catch

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I figured it would be unlikely for a custom hook to be called useAbuseState or something like that.

Not too happy about this heuristic but not sure what’s better.

let ReactTestRenderer;
let ReactDebugTools;

describe('ReactHooksInspectionIntergration', () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a custom hook integration test?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice to have some basic testing of the stack parsing+intersection logic too.

// Warm up the cache so that it doesn't consume the currentHook.
getPrimitiveStackCache();
let type = fiber.type;
let props = fiber.memoizedProps;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this have default props resolved?

@bvaughn
Copy link
Contributor

bvaughn commented Nov 5, 2018

Ideally we want this in DevTools but it requires so much implementation details that I don't think we'll want to maintain this with multi-version support. We'll probably have to move to snapshot versions of this package in DevTools, or just snapshoting all of DevTools.

Related issue: facebook/react-devtools/issues/1214

If we were to rearchitect this package in the way I propose on that issue, would there still be value in releasing react-debug-tools as its own package?

For each primitive hook in the log, process the stack trace by removing any shared ancestors with the root of the inspection function. This ensures that the bottom of the stack is the render function. Also, for that particular primitive, remove the top of the stack that is shared with the stack when that primitive is called stand alone from a function component. This removes the call to the primitive itself.

Nice! 👏

"size": 95936,
"gzip": 25258
"size": 98406,
"gzip": 25783
Copy link
Contributor

@bvaughn bvaughn Nov 5, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we normally include results.json in PRs (to avoid causing merge conflicts).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should just delete it and stop generating it on local builds :P

let ReactTestRenderer;
let ReactDebugTools;

describe('ReactHooksInspectionIntergration', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice to have some basic testing of the stack parsing+intersection logic too.

Dispatcher.useEffect(() => {});
Dispatcher.useImperativeMethods(undefined, () => null);
Dispatcher.useCallback(() => {});
Dispatcher.useMemo(() => null);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice if we had an automated check to ensure that the set of fake hooks here stays in sync with the set of real ones.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably need a Flow type for this. There's not only one real one. There are at least three (Fiber, old SSR, new SSR).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And shallow one!


function useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice if we had an automated check to ensure these hook signatures stayed in-sync with the real ones.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dispatcher.useImperativeMethods(undefined, () => null);
Dispatcher.useCallback(() => {});
Dispatcher.useMemo(() => null);
} finally {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we swallow errors in this way? An error in an given hook would block parsing of all subsequent hooks.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't swallow errors. This is a finally call.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I chose my words poorly.

I was trying to point out that an error halfway through the hooks initialization would leave primitiveStackCache halfway populated with stack info, which seems weird. What's the point? It seems like we should just fail hard (no primitiveStackCache, future calls to this method also fail)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bvaughn We only set primitiveStackCache if it doesn't throw.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤪 Right.

if (hook !== null) {
currentHook = hook.next;
}
return hook;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems a little confusing that a function named nextHook returns the current hook 😄

//
// 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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is great. ^

Would be nice to have some tests for these cases.

// 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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just read the value from contextMap directly in readContext and useContext (rather than temporarily overriding context._currentValue)?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The contextMap stores the shadowed value, not the current one.

It's nice to use the same mechanism as Fiber because it allows the inspectHook thing to be called in a context where it context has already been set up with some other mechanism.

@sebmarkbage sebmarkbage merged commit fd1256a into facebook:master Nov 5, 2018
jetoneza pushed a commit to jetoneza/react that referenced this pull request Jan 23, 2019
* 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
n8schloss pushed a commit to n8schloss/react that referenced this pull request Jan 31, 2019
* 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
@a-x-
Copy link

a-x- commented Aug 9, 2019

Can you help me to clarify, what is useInspect? there is not any useInspect in react source, google also don't know anything about it

@bvaughn
Copy link
Contributor

bvaughn commented Aug 9, 2019

@a-x- The hook Sebastian was referring to was later added as useDebugValue (#14559)

You can read about it here: https://reactjs.org/docs/hooks-reference.html#usedebugvalue

NMinhNguyen referenced this pull request in enzymejs/react-shallow-renderer Jan 29, 2020
* 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants