mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 12:20:20 +01:00
<!-- Thanks for submitting a pull request! We appreciate you spending the time to work on these changes. Please provide enough information so that others can review your pull request. The three fields below are mandatory. Before submitting a pull request, please make sure the following is done: 1. Fork [the repository](https://github.com/facebook/react) and create your branch from `main`. 2. Run `yarn` in the repository root. 3. If you've fixed a bug or added code that should be tested, add tests! 4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch TestName` is helpful in development. 5. Run `yarn test --prod` to test in the production environment. It supports the same options as `yarn test`. 6. If you need a debugger, run `yarn test --debug --watch TestName`, open `chrome://inspect`, and press "Inspect". 7. Format your code with [prettier](https://github.com/prettier/prettier) (`yarn prettier`). 8. Make sure your code lints (`yarn lint`). Tip: `yarn linc` to only check changed files. 9. Run the [Flow](https://flowtype.org/) type checks (`yarn flow`). 10. If you haven't already, complete the CLA. Learn more about contributing: https://reactjs.org/docs/how-to-contribute.html --> ## Summary This PR adds support for displaying the names of changed hooks directly in the Profiler tab, making it easier to identify specific updates. A `HookChangeSummary` component has been introduced to show these hook names, with a `displayMode` prop that toggles between `“compact”` for tooltips and `“detailed”` for more in-depth views. This keeps tooltip summaries concise while allowing for a full breakdown where needed. This functionality also respects the `“Always parse hook names from source”` setting from the Component inspector, as it uses the same caching mechanism already in place for the Components tab. Additionally, even without hook names parsed, the Profiler will now display hook types (like `State`, `Callback`, etc.) based on data from `inspectedElement`. To enable this across the DevTools, `InspectedElementContext` has been moved higher in the component tree, allowing it to be shared between the Profiler and Components tabs. This update allows hook name data to be reused across tabs without duplication. Additionally, a `getAlreadyLoadedHookNames` helper function was added to efficiently access cached hook names, reducing the need for repeated fetching when displaying changes. These changes improve the ability to track specific hook updates within the Profiler tab, making it clearer to see what’s changed. ### Before Previously, the Profiler tab displayed only the IDs of changed hooks, as shown below: <img width="350" alt="Screenshot 2024-11-01 at 12 02 21_cropped" src="https://github.com/user-attachments/assets/7a5f5f67-f1c8-4261-9ba3-1c76c9a88af3"> ### After (without hook names parsed) When hook names aren’t parsed, custom hooks and hook types are displayed based on the inspectedElement data: <img width="350" alt="Screenshot 2024-11-01 at 12 03 09_cropped" src="https://github.com/user-attachments/assets/ed857a6d-e6ef-4e5b-982c-bf30c2d8a7e2"> ### After (with hook names parsed) Once hook names are fully parsed, the Profiler tab provides a complete breakdown of specific hooks that have changed: <img width="350" alt="Screenshot 2024-11-01 at 12 03 14_cropped" src="https://github.com/user-attachments/assets/1ddfcc35-7474-4f4d-a084-f4e9f993a5bf"> This should resolve #21856 🎉
232 lines
6.2 KiB
JavaScript
232 lines
6.2 KiB
JavaScript
/**
|
|
* 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.
|
|
*
|
|
* @flow
|
|
*/
|
|
|
|
import {__DEBUG__} from 'react-devtools-shared/src/constants';
|
|
|
|
import type {HooksTree} from 'react-debug-tools/src/ReactDebugHooks';
|
|
import type {Thenable, Wakeable} from 'shared/ReactTypes';
|
|
import type {
|
|
Element,
|
|
HookNames,
|
|
HookSourceLocationKey,
|
|
} from 'react-devtools-shared/src/frontend/types';
|
|
import type {HookSource} from 'react-debug-tools/src/ReactDebugHooks';
|
|
import type {FetchFileWithCaching} from 'react-devtools-shared/src/devtools/views/Components/FetchFileWithCachingContext';
|
|
import {withCallbackPerfMeasurements} from './PerformanceLoggingUtils';
|
|
import {logEvent} from './Logger';
|
|
|
|
const TIMEOUT = 30000;
|
|
|
|
const Pending = 0;
|
|
const Resolved = 1;
|
|
const Rejected = 2;
|
|
|
|
type PendingRecord = {
|
|
status: 0,
|
|
value: Wakeable,
|
|
};
|
|
|
|
type ResolvedRecord<T> = {
|
|
status: 1,
|
|
value: T,
|
|
};
|
|
|
|
type RejectedRecord = {
|
|
status: 2,
|
|
value: null,
|
|
};
|
|
|
|
type Record<T> = PendingRecord | ResolvedRecord<T> | RejectedRecord;
|
|
|
|
function readRecord<T>(record: Record<T>): ResolvedRecord<T> | RejectedRecord {
|
|
if (record.status === Resolved) {
|
|
// This is just a type refinement.
|
|
return record;
|
|
} else if (record.status === Rejected) {
|
|
// This is just a type refinement.
|
|
return record;
|
|
} else {
|
|
throw record.value;
|
|
}
|
|
}
|
|
|
|
type LoadHookNamesFunction = (
|
|
hookLog: HooksTree,
|
|
fetchFileWithCaching: FetchFileWithCaching | null,
|
|
) => Thenable<HookNames>;
|
|
|
|
// This is intentionally a module-level Map, rather than a React-managed one.
|
|
// Otherwise, refreshing the inspected element cache would also clear this cache.
|
|
// TODO Rethink this if the React API constraints change.
|
|
// See https://github.com/reactwg/react-18/discussions/25#discussioncomment-980435
|
|
let map: WeakMap<Element, Record<HookNames>> = new WeakMap();
|
|
|
|
export function hasAlreadyLoadedHookNames(element: Element): boolean {
|
|
const record = map.get(element);
|
|
return record != null && record.status === Resolved;
|
|
}
|
|
|
|
export function getAlreadyLoadedHookNames(element: Element): HookNames | null {
|
|
const record = map.get(element);
|
|
if (record != null && record.status === Resolved) {
|
|
return record.value;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function loadHookNames(
|
|
element: Element,
|
|
hooksTree: HooksTree,
|
|
loadHookNamesFunction: LoadHookNamesFunction,
|
|
fetchFileWithCaching: FetchFileWithCaching | null,
|
|
): HookNames | null {
|
|
let record = map.get(element);
|
|
|
|
if (__DEBUG__) {
|
|
console.groupCollapsed('loadHookNames() record:');
|
|
console.log(record);
|
|
console.groupEnd();
|
|
}
|
|
|
|
if (!record) {
|
|
const callbacks = new Set<() => mixed>();
|
|
const wakeable: Wakeable = {
|
|
then(callback: () => mixed) {
|
|
callbacks.add(callback);
|
|
},
|
|
|
|
// Optional property used by Timeline:
|
|
displayName: `Loading hook names for ${element.displayName || 'Unknown'}`,
|
|
};
|
|
|
|
let timeoutID: $FlowFixMe | null;
|
|
let didTimeout = false;
|
|
let status = 'unknown';
|
|
let resolvedHookNames: HookNames | null = null;
|
|
|
|
const wake = () => {
|
|
if (timeoutID) {
|
|
clearTimeout(timeoutID);
|
|
timeoutID = null;
|
|
}
|
|
|
|
// This assumes they won't throw.
|
|
callbacks.forEach(callback => callback());
|
|
callbacks.clear();
|
|
};
|
|
|
|
const handleLoadComplete = (durationMs: number): void => {
|
|
// Log duration for parsing hook names
|
|
logEvent({
|
|
event_name: 'load-hook-names',
|
|
event_status: status,
|
|
duration_ms: durationMs,
|
|
inspected_element_display_name: element.displayName,
|
|
inspected_element_number_of_hooks: resolvedHookNames?.size ?? null,
|
|
});
|
|
};
|
|
|
|
const newRecord: Record<HookNames> = (record = {
|
|
status: Pending,
|
|
value: wakeable,
|
|
});
|
|
|
|
withCallbackPerfMeasurements(
|
|
'loadHookNames',
|
|
done => {
|
|
loadHookNamesFunction(hooksTree, fetchFileWithCaching).then(
|
|
function onSuccess(hookNames) {
|
|
if (didTimeout) {
|
|
return;
|
|
}
|
|
|
|
if (__DEBUG__) {
|
|
console.log('[hookNamesCache] onSuccess() hookNames:', hookNames);
|
|
}
|
|
|
|
if (hookNames) {
|
|
const resolvedRecord =
|
|
((newRecord: any): ResolvedRecord<HookNames>);
|
|
resolvedRecord.status = Resolved;
|
|
resolvedRecord.value = hookNames;
|
|
} else {
|
|
const notFoundRecord = ((newRecord: any): RejectedRecord);
|
|
notFoundRecord.status = Rejected;
|
|
notFoundRecord.value = null;
|
|
}
|
|
|
|
status = 'success';
|
|
resolvedHookNames = hookNames;
|
|
done();
|
|
wake();
|
|
},
|
|
function onError(error) {
|
|
if (didTimeout) {
|
|
return;
|
|
}
|
|
|
|
if (__DEBUG__) {
|
|
console.log('[hookNamesCache] onError()');
|
|
}
|
|
|
|
console.error(error);
|
|
|
|
const thrownRecord = ((newRecord: any): RejectedRecord);
|
|
thrownRecord.status = Rejected;
|
|
thrownRecord.value = null;
|
|
|
|
status = 'error';
|
|
done();
|
|
wake();
|
|
},
|
|
);
|
|
|
|
// Eventually timeout and stop trying to load names.
|
|
timeoutID = setTimeout(function onTimeout() {
|
|
if (__DEBUG__) {
|
|
console.log('[hookNamesCache] onTimeout()');
|
|
}
|
|
|
|
timeoutID = null;
|
|
|
|
didTimeout = true;
|
|
|
|
const timedoutRecord = ((newRecord: any): RejectedRecord);
|
|
timedoutRecord.status = Rejected;
|
|
timedoutRecord.value = null;
|
|
|
|
status = 'timeout';
|
|
done();
|
|
wake();
|
|
}, TIMEOUT);
|
|
},
|
|
handleLoadComplete,
|
|
);
|
|
map.set(element, record);
|
|
}
|
|
|
|
const response = readRecord(record).value;
|
|
return response;
|
|
}
|
|
|
|
export function getHookSourceLocationKey({
|
|
fileName,
|
|
lineNumber,
|
|
columnNumber,
|
|
}: HookSource): HookSourceLocationKey {
|
|
if (fileName == null || lineNumber == null || columnNumber == null) {
|
|
throw Error('Hook source code location not found.');
|
|
}
|
|
return `${fileName}:${lineNumber}:${columnNumber}`;
|
|
}
|
|
|
|
export function clearHookNamesCache(): void {
|
|
map = new WeakMap();
|
|
}
|