-
Notifications
You must be signed in to change notification settings - Fork 46.9k
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
Conversation
Details of bundled changes.Comparing: 8eca0ef...7006c4b scheduler
react-debug-tools
Generated by 🚫 dangerJS |
There was a problem hiding this 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.** |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
, and it
packages/react-debug-tools/index.js
Outdated
* LICENSE file in the root directory of this source tree. | ||
*/ | ||
|
||
// This entry point is intentionally not typed. It exists only for third-party |
There was a problem hiding this comment.
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('.'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lastIndexOf?
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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', () => { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
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.
Just in case. I don't know of any scenario where this can happen.
9e51849
to
7006c4b
Compare
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
Nice! 👏 |
"size": 95936, | ||
"gzip": 25258 | ||
"size": 98406, | ||
"gzip": 25783 |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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', () => { |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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>>] { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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
)?
There was a problem hiding this comment.
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.
* 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
* 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
Can you help me to clarify, what is |
@a-x- The hook Sebastian was referring to was later added as You can read about it here: https://reactjs.org/docs/hooks-reference.html#usedebugvalue |
* 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
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:
his will give us a list of all primitive hooks and their stacks.
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)