-
Notifications
You must be signed in to change notification settings - Fork 47.3k
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
Devtools: Add support for useFormStatus #28413
Changes from 6 commits
b66beff
83e9d3c
8910c69
daa026a
eeece92
274e1cc
de67894
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,6 +21,7 @@ import type { | |
Fiber, | ||
Dispatcher as DispatcherType, | ||
} from 'react-reconciler/src/ReactInternalTypes'; | ||
import type {TransitionStatus} from 'react-reconciler/src/ReactFiberConfig'; | ||
|
||
import ErrorStackParser from 'error-stack-parser'; | ||
import assign from 'shared/assign'; | ||
|
@@ -134,6 +135,11 @@ function getPrimitiveStackCache(): Map<string, Array<any>> { | |
} | ||
|
||
Dispatcher.useId(); | ||
|
||
if (typeof Dispatcher.useHostTransitionStatus === 'function') { | ||
// This type check is for Flow only. | ||
Dispatcher.useHostTransitionStatus(); | ||
} | ||
} finally { | ||
readHookLog = hookLog; | ||
hookLog = []; | ||
|
@@ -711,6 +717,27 @@ function useActionState<S, P>( | |
return [state, (payload: P) => {}, false]; | ||
} | ||
|
||
function useHostTransitionStatus(): TransitionStatus { | ||
const status = readContext<TransitionStatus>( | ||
// $FlowFixMe[prop-missing] `readContext` only needs _currentValue | ||
({ | ||
// $FlowFixMe[incompatible-cast] TODO: Incorrect bottom value without access to Fiber config. | ||
_currentValue: null, | ||
}: ReactContext<TransitionStatus>), | ||
); | ||
|
||
hookLog.push({ | ||
displayName: null, | ||
primitive: 'HostTransitionStatus', | ||
stackError: new Error(), | ||
value: status, | ||
debugInfo: null, | ||
dispatcherHookName: 'HostTransitionStatus', | ||
}); | ||
|
||
return status; | ||
} | ||
|
||
const Dispatcher: DispatcherType = { | ||
use, | ||
readContext, | ||
|
@@ -734,6 +761,7 @@ const Dispatcher: DispatcherType = { | |
useId, | ||
useFormState, | ||
useActionState, | ||
useHostTransitionStatus, | ||
}; | ||
|
||
// create a proxy to throw a custom error | ||
|
@@ -854,11 +882,6 @@ function findPrimitiveIndex(hookStack: any, hook: HookLogEntry) { | |
isReactWrapper(hookStack[i].functionName, hook.dispatcherHookName) | ||
) { | ||
i++; | ||
} | ||
if ( | ||
i < hookStack.length - 1 && | ||
isReactWrapper(hookStack[i].functionName, hook.dispatcherHookName) | ||
) { | ||
i++; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Makes sense to skip 2 frames here, but I would also keep There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can't think of a case where we wouldn't have two frames. That would mean the dispatcher call is inlined into usercode. Maybe this can happen but at that point we'll never be able to recover the original React hook name. I'll add a check regardless since it's better to display the dispatcher name than crashing. |
||
} | ||
return i; | ||
|
@@ -997,7 +1020,8 @@ function buildTree( | |
primitive === 'Context (use)' || | ||
primitive === 'DebugValue' || | ||
primitive === 'Promise' || | ||
primitive === 'Unresolved' | ||
primitive === 'Unresolved' || | ||
primitive === 'HostTransitionStatus' | ||
? null | ||
: nativeHookID++; | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
/** | ||
* Copyright (c) Meta Platforms, Inc. and 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 jsdom | ||
*/ | ||
|
||
'use strict'; | ||
|
||
let React; | ||
let ReactDOM; | ||
let ReactDOMClient; | ||
let ReactDebugTools; | ||
let act; | ||
|
||
function normalizeSourceLoc(tree) { | ||
tree.forEach(node => { | ||
if (node.hookSource) { | ||
node.hookSource.fileName = '**'; | ||
node.hookSource.lineNumber = 0; | ||
node.hookSource.columnNumber = 0; | ||
} | ||
normalizeSourceLoc(node.subHooks); | ||
}); | ||
return tree; | ||
} | ||
|
||
describe('ReactHooksInspectionIntegration', () => { | ||
beforeEach(() => { | ||
jest.resetModules(); | ||
React = require('react'); | ||
ReactDOM = require('react-dom'); | ||
ReactDOMClient = require('react-dom/client'); | ||
act = require('internal-test-utils').act; | ||
ReactDebugTools = require('react-debug-tools'); | ||
}); | ||
|
||
it('should support useFormStatus hook', async () => { | ||
function FormStatus() { | ||
const status = ReactDOM.useFormStatus(); | ||
React.useMemo(() => 'memo', []); | ||
React.useMemo(() => 'not used', []); | ||
|
||
return JSON.stringify(status); | ||
} | ||
|
||
const treeWithoutFiber = ReactDebugTools.inspectHooks(FormStatus); | ||
expect(normalizeSourceLoc(treeWithoutFiber)).toEqual([ | ||
{ | ||
debugInfo: null, | ||
hookSource: { | ||
columnNumber: 0, | ||
fileName: '**', | ||
functionName: 'FormStatus', | ||
lineNumber: 0, | ||
}, | ||
id: null, | ||
isStateEditable: false, | ||
name: 'FormStatus', | ||
subHooks: [], | ||
value: null, | ||
}, | ||
{ | ||
debugInfo: null, | ||
hookSource: { | ||
columnNumber: 0, | ||
fileName: '**', | ||
functionName: 'FormStatus', | ||
lineNumber: 0, | ||
}, | ||
id: 0, | ||
isStateEditable: false, | ||
name: 'Memo', | ||
subHooks: [], | ||
value: 'memo', | ||
}, | ||
{ | ||
debugInfo: null, | ||
hookSource: { | ||
columnNumber: 0, | ||
fileName: '**', | ||
functionName: 'FormStatus', | ||
lineNumber: 0, | ||
}, | ||
id: 1, | ||
isStateEditable: false, | ||
name: 'Memo', | ||
subHooks: [], | ||
value: 'not used', | ||
}, | ||
]); | ||
|
||
const root = ReactDOMClient.createRoot(document.createElement('div')); | ||
|
||
await act(() => { | ||
root.render( | ||
<form> | ||
<FormStatus /> | ||
</form>, | ||
); | ||
}); | ||
|
||
// Implementation detail. Feel free to adjust the position of the Fiber in the tree. | ||
const formStatusFiber = root._internalRoot.current.child.child; | ||
const treeWithFiber = ReactDebugTools.inspectHooksOfFiber(formStatusFiber); | ||
expect(normalizeSourceLoc(treeWithFiber)).toEqual([ | ||
{ | ||
debugInfo: null, | ||
hookSource: { | ||
columnNumber: 0, | ||
fileName: '**', | ||
functionName: 'FormStatus', | ||
lineNumber: 0, | ||
}, | ||
id: null, | ||
isStateEditable: false, | ||
name: 'FormStatus', | ||
subHooks: [], | ||
value: null, | ||
}, | ||
{ | ||
debugInfo: null, | ||
hookSource: { | ||
columnNumber: 0, | ||
fileName: '**', | ||
functionName: 'FormStatus', | ||
lineNumber: 0, | ||
}, | ||
id: 0, | ||
isStateEditable: false, | ||
name: 'Memo', | ||
subHooks: [], | ||
value: 'memo', | ||
}, | ||
{ | ||
debugInfo: null, | ||
hookSource: { | ||
columnNumber: 0, | ||
fileName: '**', | ||
functionName: 'FormStatus', | ||
lineNumber: 0, | ||
}, | ||
id: 1, | ||
isStateEditable: false, | ||
name: 'Memo', | ||
subHooks: [], | ||
value: 'not used', | ||
}, | ||
]); | ||
}); | ||
}); |
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.
Makes