mirror of
https://github.com/facebook/react.git
synced 2026-02-24 04:33:04 +00:00
There are not so many changes, most of them are changing imports,
because I've moved types for UI in a single file.
In https://github.com/facebook/react/pull/27357 I've added support for
pausing polling events: when user inspects an element, we start polling
React DevTools backend for updates in props / state. If user switches
tabs, extension's service worker can be killed by browser and this
polling will start spamming errors.
What I've missed is that we also have a separate call for this API, but
which is executed only once when user selects an element. We don't
handle promise rejection here and this can lead to some errors when user
selects an element and switches tabs right after it.
The only change here is that this API now has
`shouldListenToPauseEvents` param, which is `true` for polling, so we
will pause polling once user switches tabs. It is `false` by default, so
we won't pause initial call by accident.
af8beeebf6/packages/react-devtools-shared/src/backendAPI.js (L96)
172 lines
5.6 KiB
JavaScript
172 lines
5.6 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 LRU from 'lru-cache';
|
|
import {
|
|
convertInspectedElementBackendToFrontend,
|
|
hydrateHelper,
|
|
inspectElement as inspectElementAPI,
|
|
} from 'react-devtools-shared/src/backendAPI';
|
|
import {fillInPath} from 'react-devtools-shared/src/hydration';
|
|
|
|
import type {LRUCache} from 'react-devtools-shared/src/frontend/types';
|
|
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
|
|
import type {
|
|
InspectElementError,
|
|
InspectElementFullData,
|
|
InspectElementHydratedPath,
|
|
} from 'react-devtools-shared/src/backend/types';
|
|
import UserError from 'react-devtools-shared/src/errors/UserError';
|
|
import UnknownHookError from 'react-devtools-shared/src/errors/UnknownHookError';
|
|
import type {
|
|
Element,
|
|
InspectedElement as InspectedElementFrontend,
|
|
InspectedElementResponseType,
|
|
InspectedElementPath,
|
|
} from 'react-devtools-shared/src/frontend/types';
|
|
|
|
// Maps element ID to inspected data.
|
|
// We use an LRU for this rather than a WeakMap because of how the "no-change" optimization works.
|
|
// When the frontend polls the backend for an update on the element that's currently inspected,
|
|
// the backend will send a "no-change" message if the element hasn't updated (rendered) since the last time it was asked.
|
|
// In this case, the frontend cache should reuse the previous (cached) value.
|
|
// Using a WeakMap keyed on Element generally works well for this, since Elements are mutable and stable in the Store.
|
|
// This doens't work properly though when component filters are changed,
|
|
// because this will cause the Store to dump all roots and re-initialize the tree (recreating the Element objects).
|
|
// So instead we key on Element ID (which is stable in this case) and use an LRU for eviction.
|
|
const inspectedElementCache: LRUCache<number, InspectedElementFrontend> =
|
|
new LRU({
|
|
max: 25,
|
|
});
|
|
|
|
type InspectElementReturnType = [
|
|
InspectedElementFrontend,
|
|
InspectedElementResponseType,
|
|
];
|
|
|
|
export function inspectElement(
|
|
bridge: FrontendBridge,
|
|
element: Element,
|
|
path: InspectedElementPath | null,
|
|
rendererID: number,
|
|
shouldListenToPauseEvents: boolean = false,
|
|
): Promise<InspectElementReturnType> {
|
|
const {id} = element;
|
|
|
|
// This could indicate that the DevTools UI has been closed and reopened.
|
|
// The in-memory cache will be clear but the backend still thinks we have cached data.
|
|
// In this case, we need to tell it to resend the full data.
|
|
const forceFullData = !inspectedElementCache.has(id);
|
|
|
|
return inspectElementAPI(
|
|
bridge,
|
|
forceFullData,
|
|
id,
|
|
path,
|
|
rendererID,
|
|
shouldListenToPauseEvents,
|
|
).then((data: any) => {
|
|
const {type} = data;
|
|
|
|
let inspectedElement;
|
|
switch (type) {
|
|
case 'error': {
|
|
const {message, stack, errorType} = ((data: any): InspectElementError);
|
|
|
|
// create a different error class for each error type
|
|
// and keep useful information from backend.
|
|
let error;
|
|
if (errorType === 'user') {
|
|
error = new UserError(message);
|
|
} else if (errorType === 'unknown-hook') {
|
|
error = new UnknownHookError(message);
|
|
} else {
|
|
error = new Error(message);
|
|
}
|
|
// The backend's stack (where the error originated) is more meaningful than this stack.
|
|
error.stack = stack || error.stack;
|
|
|
|
throw error;
|
|
}
|
|
|
|
case 'no-change':
|
|
// This is a no-op for the purposes of our cache.
|
|
inspectedElement = inspectedElementCache.get(id);
|
|
if (inspectedElement != null) {
|
|
return [inspectedElement, type];
|
|
}
|
|
|
|
// We should only encounter this case in the event of a bug.
|
|
throw Error(`Cached data for element "${id}" not found`);
|
|
|
|
case 'not-found':
|
|
// This is effectively a no-op.
|
|
// If the Element is still in the Store, we can eagerly remove it from the Map.
|
|
inspectedElementCache.del(id);
|
|
|
|
throw Error(`Element "${id}" not found`);
|
|
|
|
case 'full-data':
|
|
const fullData = ((data: any): InspectElementFullData);
|
|
|
|
// New data has come in.
|
|
// We should replace the data in our local mutable copy.
|
|
inspectedElement = convertInspectedElementBackendToFrontend(
|
|
fullData.value,
|
|
);
|
|
|
|
inspectedElementCache.set(id, inspectedElement);
|
|
|
|
return [inspectedElement, type];
|
|
|
|
case 'hydrated-path':
|
|
const hydratedPathData = ((data: any): InspectElementHydratedPath);
|
|
const {value} = hydratedPathData;
|
|
|
|
// A path has been hydrated.
|
|
// Merge it with the latest copy we have locally and resolve with the merged value.
|
|
inspectedElement = inspectedElementCache.get(id) || null;
|
|
if (inspectedElement !== null) {
|
|
// Clone element
|
|
inspectedElement = {...inspectedElement};
|
|
|
|
// Merge hydrated data
|
|
if (path != null) {
|
|
fillInPath(
|
|
inspectedElement,
|
|
value,
|
|
path,
|
|
hydrateHelper(value, path),
|
|
);
|
|
}
|
|
|
|
inspectedElementCache.set(id, inspectedElement);
|
|
|
|
return [inspectedElement, type];
|
|
}
|
|
break;
|
|
|
|
default:
|
|
// Should never happen.
|
|
if (__DEV__) {
|
|
console.error(
|
|
`Unexpected inspected element response data: "${type}"`,
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
|
|
throw Error(`Unable to inspect element with id "${id}"`);
|
|
});
|
|
}
|
|
|
|
export function clearCacheForTests(): void {
|
|
inspectedElementCache.reset();
|
|
}
|