mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Merge pull request #196 from bvaughn/selected-element-suspense
Convert inspected element (right panel) to use Suspense
This commit is contained in:
+2
-2
@@ -109,7 +109,7 @@
|
||||
"fbjs": "0.5.1",
|
||||
"fbjs-scripts": "0.7.0",
|
||||
"firefox-profile": "^1.0.2",
|
||||
"flow-bin": "^0.96.0",
|
||||
"flow-bin": "^0.97.0",
|
||||
"fs-extra": "^3.0.1",
|
||||
"gh-pages": "^1.0.0",
|
||||
"html2canvas": "^1.0.0-alpha.12",
|
||||
@@ -135,7 +135,7 @@
|
||||
"react-virtualized-auto-sizer": "^1.0.2",
|
||||
"react-window": "^1.8.0",
|
||||
"request-promise": "^4.2.4",
|
||||
"scheduler": "^0.14.0-alpha.0",
|
||||
"scheduler": "0.0.0-4221565e1",
|
||||
"semver": "^5.5.1",
|
||||
"style-loader": "^0.23.1",
|
||||
"web-ext": "^3.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
|
||||
import { createResource, invalidateResources } from './cache';
|
||||
import { createResource } from './cache';
|
||||
import Store from './store';
|
||||
import {
|
||||
getCommitTree,
|
||||
@@ -92,6 +92,7 @@ export default class ProfilingCache {
|
||||
|
||||
CommitDetails: Resource<
|
||||
CommitDetailsParams,
|
||||
string,
|
||||
CommitDetailsFrontend
|
||||
> = createResource(
|
||||
({ commitIndex, rendererID, rootID }: CommitDetailsParams) => {
|
||||
@@ -136,6 +137,7 @@ export default class ProfilingCache {
|
||||
|
||||
FiberCommits: Resource<
|
||||
FiberCommitsParams,
|
||||
string,
|
||||
FiberCommitsFrontend
|
||||
> = createResource(
|
||||
({ fiberID, rendererID, rootID }: FiberCommitsParams) => {
|
||||
@@ -168,6 +170,7 @@ export default class ProfilingCache {
|
||||
|
||||
Interactions: Resource<
|
||||
InteractionsParams,
|
||||
number,
|
||||
InteractionsFrontend
|
||||
> = createResource(
|
||||
({ rendererID, rootID }: InteractionsParams) => {
|
||||
@@ -198,6 +201,7 @@ export default class ProfilingCache {
|
||||
|
||||
ProfilingSummary: Resource<
|
||||
ProfilingSummaryParams,
|
||||
number,
|
||||
ProfilingSummaryFrontend
|
||||
> = createResource(
|
||||
({ rendererID, rootID }: ProfilingSummaryParams) => {
|
||||
@@ -293,8 +297,11 @@ export default class ProfilingCache {
|
||||
});
|
||||
|
||||
invalidate() {
|
||||
// Invalidate Susepnse caches.
|
||||
invalidateResources();
|
||||
// Invalidate Suspense caches.
|
||||
this.CommitDetails.clear();
|
||||
this.FiberCommits.clear();
|
||||
this.Interactions.clear();
|
||||
this.ProfilingSummary.clear();
|
||||
|
||||
// Invalidate non-Suspense caches too.
|
||||
invalidateCommitTrees();
|
||||
|
||||
+69
-32
@@ -12,7 +12,7 @@ import React, { createContext } from 'react';
|
||||
// The size of this cache is bounded by how many renders were profiled,
|
||||
// and it will be fully reset between profiling sessions.
|
||||
|
||||
type Thenable<T> = {
|
||||
export type Thenable<T> = {
|
||||
then(resolve: (T) => mixed, reject: (mixed) => mixed): mixed,
|
||||
};
|
||||
|
||||
@@ -25,9 +25,9 @@ type PendingResult = {|
|
||||
value: Suspender,
|
||||
|};
|
||||
|
||||
type ResolvedResult<V> = {|
|
||||
type ResolvedResult<Value> = {|
|
||||
status: 1,
|
||||
value: V,
|
||||
value: Value,
|
||||
|};
|
||||
|
||||
type RejectedResult = {|
|
||||
@@ -35,11 +35,14 @@ type RejectedResult = {|
|
||||
value: mixed,
|
||||
|};
|
||||
|
||||
type Result<V> = PendingResult | ResolvedResult<V> | RejectedResult;
|
||||
type Result<Value> = PendingResult | ResolvedResult<Value> | RejectedResult;
|
||||
|
||||
export type Resource<I, V> = {
|
||||
read(I): V,
|
||||
preload(I): void,
|
||||
export type Resource<Input, Key, Value> = {
|
||||
clear(): void,
|
||||
invalidate(Key): void,
|
||||
read(Input): Value,
|
||||
preload(Input): void,
|
||||
write(Key, Value): void,
|
||||
};
|
||||
|
||||
const Pending = 0;
|
||||
@@ -61,32 +64,45 @@ function readContext(Context, observedBits) {
|
||||
return dispatcher.readContext(Context, observedBits);
|
||||
}
|
||||
|
||||
function identityHashFn(input) {
|
||||
return input;
|
||||
}
|
||||
|
||||
const CacheContext = createContext(null);
|
||||
|
||||
const entries: Map<Resource<any, any>, Map<any, any>> = new Map();
|
||||
type Config = {
|
||||
useWeakMap?: boolean,
|
||||
};
|
||||
|
||||
function accessResult<I, K, V>(
|
||||
resource: any,
|
||||
fetch: I => Thenable<V>,
|
||||
input: I,
|
||||
key: K
|
||||
): Result<V> {
|
||||
let entriesForResource = entries.get(resource);
|
||||
const entries: Map<
|
||||
Resource<any, any, any>,
|
||||
Map<any, any> | WeakMap<any, any>
|
||||
> = new Map();
|
||||
const resourceConfigs: Map<Resource<any, any, any>, Config> = new Map();
|
||||
|
||||
function getEntriesForResource(
|
||||
resource: any
|
||||
): Map<any, any> | WeakMap<any, any> {
|
||||
let entriesForResource = ((entries.get(resource): any): Map<any, any>);
|
||||
if (entriesForResource === undefined) {
|
||||
entriesForResource = new Map();
|
||||
const config = resourceConfigs.get(resource);
|
||||
entriesForResource =
|
||||
config !== undefined && config.useWeakMap ? new WeakMap() : new Map();
|
||||
entries.set(resource, entriesForResource);
|
||||
}
|
||||
let entry = entriesForResource.get(key);
|
||||
return entriesForResource;
|
||||
}
|
||||
|
||||
function accessResult<Input, Key, Value>(
|
||||
resource: any,
|
||||
fetch: Input => Thenable<Value>,
|
||||
input: Input,
|
||||
key: Key
|
||||
): Result<Value> {
|
||||
const entriesForResource = getEntriesForResource(resource);
|
||||
const entry = entriesForResource.get(key);
|
||||
if (entry === undefined) {
|
||||
const thenable = fetch(input);
|
||||
thenable.then(
|
||||
value => {
|
||||
if (newResult.status === Pending) {
|
||||
const resolvedResult: ResolvedResult<V> = (newResult: any);
|
||||
const resolvedResult: ResolvedResult<Value> = (newResult: any);
|
||||
resolvedResult.status = Resolved;
|
||||
resolvedResult.value = value;
|
||||
}
|
||||
@@ -110,21 +126,28 @@ function accessResult<I, K, V>(
|
||||
}
|
||||
}
|
||||
|
||||
export function createResource<I, K: string | number, V>(
|
||||
fetch: I => Thenable<V>,
|
||||
maybeHashInput?: I => K
|
||||
): Resource<I, V> {
|
||||
const hashInput: I => K =
|
||||
maybeHashInput !== undefined ? maybeHashInput : (identityHashFn: any);
|
||||
|
||||
export function createResource<Input, Key, Value>(
|
||||
fetch: Input => Thenable<Value>,
|
||||
hashInput: Input => Key,
|
||||
config?: Config = {}
|
||||
): Resource<Input, Key, Value> {
|
||||
const resource = {
|
||||
read(input: I): V {
|
||||
clear(): void {
|
||||
entries.delete(resource);
|
||||
},
|
||||
|
||||
invalidate(key: Key): void {
|
||||
const entriesForResource = getEntriesForResource(resource);
|
||||
entriesForResource.delete(key);
|
||||
},
|
||||
|
||||
read(input: Input): Value {
|
||||
// Prevent access outside of render.
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
readContext(CacheContext);
|
||||
|
||||
const key = hashInput(input);
|
||||
const result: Result<V> = accessResult(resource, fetch, input, key);
|
||||
const result: Result<Value> = accessResult(resource, fetch, input, key);
|
||||
switch (result.status) {
|
||||
case Pending: {
|
||||
const suspender = result.value;
|
||||
@@ -144,7 +167,7 @@ export function createResource<I, K: string | number, V>(
|
||||
}
|
||||
},
|
||||
|
||||
preload(input: I): void {
|
||||
preload(input: Input): void {
|
||||
// Prevent access outside of render.
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
readContext(CacheContext);
|
||||
@@ -152,7 +175,21 @@ export function createResource<I, K: string | number, V>(
|
||||
const key = hashInput(input);
|
||||
accessResult(resource, fetch, input, key);
|
||||
},
|
||||
|
||||
write(key: Key, value: Value): void {
|
||||
const entriesForResource = getEntriesForResource(resource);
|
||||
|
||||
const resolvedResult: ResolvedResult<Value> = {
|
||||
status: Resolved,
|
||||
value,
|
||||
};
|
||||
|
||||
entriesForResource.set(key, resolvedResult);
|
||||
},
|
||||
};
|
||||
|
||||
resourceConfigs.set(resource, config);
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
|
||||
@@ -68,8 +68,9 @@ export default class Store extends EventEmitter {
|
||||
// At least one of the injected renderers contains (DEV only) owner metadata.
|
||||
_hasOwnerMetadata: boolean = false;
|
||||
|
||||
// Map of ID to Element.
|
||||
// Elements are mutable (for now) to avoid excessive cloning during tree updates.
|
||||
// Map of ID to (mutable) Element.
|
||||
// Elements are mutated to avoid excessive cloning during tree updates.
|
||||
// The InspectedElementContext also relies on this mutability for its WeakMap usage.
|
||||
_idToElement: Map<number, Element> = new Map();
|
||||
|
||||
// The user has imported a previously exported profiling session.
|
||||
@@ -79,7 +80,7 @@ export default class Store extends EventEmitter {
|
||||
// When profiling is in progress, operations are stored so that we can later reconstruct past commit trees.
|
||||
_isProfiling: boolean = false;
|
||||
|
||||
// Suspense cache for reading profilign data.
|
||||
// Suspense cache for reading profiling data.
|
||||
_profilingCache: ProfilingCache;
|
||||
|
||||
// Map of root (id) to a list of tree mutation that occur during profiling.
|
||||
|
||||
@@ -24,3 +24,13 @@
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.Loading {
|
||||
height: 100%;
|
||||
padding-left: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: var(--font-size-sans-large);
|
||||
color: var(--color-dim);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import React, { Suspense } from 'react';
|
||||
import Tree from './Tree';
|
||||
import SelectedElement from './SelectedElement';
|
||||
import styles from './Components.css';
|
||||
import { InspectedElementContextController } from './InspectedElementContext';
|
||||
import portaledContent from '../portaledContent';
|
||||
|
||||
import styles from './Components.css';
|
||||
|
||||
function Components(_: {||}) {
|
||||
// TODO Flex wrappers below should be user resizable.
|
||||
return (
|
||||
@@ -14,10 +16,18 @@ function Components(_: {||}) {
|
||||
<Tree />
|
||||
</div>
|
||||
<div className={styles.SelectedElementWrapper}>
|
||||
<SelectedElement />
|
||||
<InspectedElementContextController>
|
||||
<Suspense fallback={<Loading />}>
|
||||
<SelectedElement />
|
||||
</Suspense>
|
||||
</InspectedElementContextController>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Loading() {
|
||||
return <div className={styles.Loading}>Loading...</div>;
|
||||
}
|
||||
|
||||
export default portaledContent(Components);
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
// @flow
|
||||
|
||||
import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { createResource } from '../../cache';
|
||||
import { BridgeContext, StoreContext } from '../context';
|
||||
import { hydrate } from 'src/hydration';
|
||||
import { TreeStateContext } from './TreeContext';
|
||||
|
||||
import type {
|
||||
DehydratedData,
|
||||
Element,
|
||||
InspectedElement,
|
||||
} from 'src/devtools/views/Components/types';
|
||||
import type { Resource, Thenable } from '../../cache';
|
||||
|
||||
type Context = {|
|
||||
read(id: number): InspectedElement | null,
|
||||
|};
|
||||
|
||||
const InspectedElementContext = createContext<Context>(((null: any): Context));
|
||||
InspectedElementContext.displayName = 'InspectedElementContext';
|
||||
|
||||
type ResolveFn = (inspectedElement: InspectedElement) => void;
|
||||
type InProgressRequest = {|
|
||||
promise: Thenable<InspectedElement>,
|
||||
resolveFn: ResolveFn,
|
||||
|};
|
||||
|
||||
const inProgressRequests: WeakMap<Element, InProgressRequest> = new WeakMap();
|
||||
const resource: Resource<Element, Element, InspectedElement> = createResource(
|
||||
(element: Element) => {
|
||||
let request = inProgressRequests.get(element);
|
||||
if (request != null) {
|
||||
return request.promise;
|
||||
}
|
||||
|
||||
let resolveFn = ((null: any): ResolveFn);
|
||||
const promise = new Promise(resolve => {
|
||||
resolveFn = resolve;
|
||||
});
|
||||
|
||||
inProgressRequests.set(element, { promise, resolveFn });
|
||||
|
||||
return promise;
|
||||
},
|
||||
(element: Element) => element,
|
||||
{ useWeakMap: true }
|
||||
);
|
||||
|
||||
type Props = {|
|
||||
children: React$Node,
|
||||
|};
|
||||
|
||||
function InspectedElementContextController({ children }: Props) {
|
||||
const bridge = useContext(BridgeContext);
|
||||
const store = useContext(StoreContext);
|
||||
|
||||
const read = useCallback(
|
||||
(id: number) => {
|
||||
const element = store.getElementByID(id);
|
||||
if (element !== null) {
|
||||
return resource.read(element);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
[store]
|
||||
);
|
||||
|
||||
// It's very important that this context consumes selectedElementID and not inspectedElementID.
|
||||
// Otherwise the effect that sends the "inspect" message across the bridge-
|
||||
// would itself be blocked by the same render that suspends (waiting for the data).
|
||||
const { selectedElementID } = useContext(TreeStateContext);
|
||||
|
||||
const [count, setCount] = useState<number>(0);
|
||||
|
||||
// This effect handler invalidates the suspense cache and schedules rendering updates with React.
|
||||
useEffect(() => {
|
||||
const onInspectedElement = (inspectedElement: InspectedElement | null) => {
|
||||
if (inspectedElement !== null) {
|
||||
const id = inspectedElement.id;
|
||||
|
||||
inspectedElement = (({
|
||||
...inspectedElement,
|
||||
context: hydrateHelper(inspectedElement.context),
|
||||
hooks: hydrateHelper(inspectedElement.hooks),
|
||||
props: hydrateHelper(inspectedElement.props),
|
||||
state: hydrateHelper(inspectedElement.state),
|
||||
}: any): InspectedElement);
|
||||
|
||||
const element = store.getElementByID(id);
|
||||
if (element !== null) {
|
||||
const request = inProgressRequests.get(element);
|
||||
if (request != null) {
|
||||
inProgressRequests.delete(element);
|
||||
request.resolveFn(inspectedElement);
|
||||
} else {
|
||||
resource.write(element, inspectedElement);
|
||||
|
||||
// Schedule update with React if the curently-selected element has been invalidated.
|
||||
if (id === selectedElementID) {
|
||||
setCount(count => count + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
bridge.addListener('inspectedElement', onInspectedElement);
|
||||
return () => bridge.removeListener('inspectedElement', onInspectedElement);
|
||||
}, [bridge, selectedElementID, store]);
|
||||
|
||||
// This effect handler polls for updates on the currently selected element.
|
||||
useEffect(() => {
|
||||
if (selectedElementID === null) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const rendererID = store.getRendererIDForElement(selectedElementID);
|
||||
|
||||
let timeoutID: TimeoutID | null = null;
|
||||
|
||||
const sendRequest = () => {
|
||||
timeoutID = null;
|
||||
|
||||
bridge.send('inspectElement', { id: selectedElementID, rendererID });
|
||||
};
|
||||
|
||||
// Send the initial inspection request.
|
||||
// We'll poll for an update in the response handler below.
|
||||
sendRequest();
|
||||
|
||||
// Update the $r variable.
|
||||
bridge.send('selectElement', { id: selectedElementID, rendererID });
|
||||
|
||||
const onInspectedElement = (inspectedElement: InspectedElement | null) => {
|
||||
if (
|
||||
inspectedElement !== null &&
|
||||
inspectedElement.id === selectedElementID
|
||||
) {
|
||||
// If this is the element we requested, wait a little bit and then ask for an update.
|
||||
timeoutID = setTimeout(sendRequest, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
bridge.addListener('inspectedElement', onInspectedElement);
|
||||
|
||||
return () => {
|
||||
bridge.removeListener('inspectedElement', onInspectedElement);
|
||||
|
||||
if (timeoutID !== null) {
|
||||
clearTimeout(timeoutID);
|
||||
}
|
||||
};
|
||||
}, [bridge, selectedElementID, store]);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({ read }),
|
||||
// Count is used to invalidate the cache and schedule an update with React.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[count, read]
|
||||
);
|
||||
|
||||
return (
|
||||
<InspectedElementContext.Provider value={value}>
|
||||
{children}
|
||||
</InspectedElementContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function hydrateHelper(dehydratedData: DehydratedData | null): Object | null {
|
||||
if (dehydratedData !== null) {
|
||||
return hydrate(dehydratedData.data, dehydratedData.cleaned);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export { InspectedElementContext, InspectedElementContextController };
|
||||
@@ -1,19 +1,13 @@
|
||||
// @flow
|
||||
|
||||
import React, {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import React, { useCallback, useContext } from 'react';
|
||||
import { TreeDispatcherContext, TreeStateContext } from './TreeContext';
|
||||
import { BridgeContext, StoreContext } from '../context';
|
||||
import Button from '../Button';
|
||||
import ButtonIcon from '../ButtonIcon';
|
||||
import HooksTree from './HooksTree';
|
||||
import InspectedElementTree from './InspectedElementTree';
|
||||
import { hydrate } from 'src/hydration';
|
||||
import { InspectedElementContext } from './InspectedElementContext';
|
||||
import ViewElementSourceContext from './ViewElementSourceContext';
|
||||
import styles from './SelectedElement.css';
|
||||
import {
|
||||
@@ -24,55 +18,59 @@ import {
|
||||
ElementTypeSuspense,
|
||||
} from '../../types';
|
||||
|
||||
import type { InspectedElement } from './types';
|
||||
import type { DehydratedData, Element } from './types';
|
||||
import type { Element, InspectedElement } from './types';
|
||||
|
||||
export type Props = {||};
|
||||
|
||||
export default function SelectedElement(_: Props) {
|
||||
const { selectedElementID } = useContext(TreeStateContext);
|
||||
const { inspectedElementID } = useContext(TreeStateContext);
|
||||
const viewElementSource = useContext(ViewElementSourceContext);
|
||||
const bridge = useContext(BridgeContext);
|
||||
const store = useContext(StoreContext);
|
||||
|
||||
const element =
|
||||
selectedElementID !== null ? store.getElementByID(selectedElementID) : null;
|
||||
const { read } = useContext(InspectedElementContext);
|
||||
|
||||
const inspectedElement = useInspectedElement(selectedElementID);
|
||||
const element =
|
||||
inspectedElementID !== null
|
||||
? store.getElementByID(inspectedElementID)
|
||||
: null;
|
||||
|
||||
const inspectedElement =
|
||||
inspectedElementID != null ? read(inspectedElementID) : null;
|
||||
|
||||
const highlightElement = useCallback(() => {
|
||||
if (element !== null && selectedElementID !== null) {
|
||||
const rendererID = store.getRendererIDForElement(selectedElementID);
|
||||
if (element !== null && inspectedElementID !== null) {
|
||||
const rendererID = store.getRendererIDForElement(inspectedElementID);
|
||||
if (rendererID !== null) {
|
||||
bridge.send('highlightElementInDOM', {
|
||||
displayName: element.displayName,
|
||||
hideAfterTimeout: true,
|
||||
id: selectedElementID,
|
||||
id: inspectedElementID,
|
||||
openNativeElementsPanel: true,
|
||||
rendererID,
|
||||
scrollIntoView: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [bridge, element, selectedElementID, store]);
|
||||
}, [bridge, element, inspectedElementID, store]);
|
||||
|
||||
const logElement = useCallback(() => {
|
||||
if (selectedElementID !== null) {
|
||||
const rendererID = store.getRendererIDForElement(selectedElementID);
|
||||
if (inspectedElementID !== null) {
|
||||
const rendererID = store.getRendererIDForElement(inspectedElementID);
|
||||
if (rendererID !== null) {
|
||||
bridge.send('logElementToConsole', {
|
||||
id: selectedElementID,
|
||||
id: inspectedElementID,
|
||||
rendererID,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [bridge, selectedElementID, store]);
|
||||
}, [bridge, inspectedElementID, store]);
|
||||
|
||||
const viewSource = useCallback(() => {
|
||||
if (viewElementSource != null && selectedElementID !== null) {
|
||||
viewElementSource(selectedElementID);
|
||||
if (viewElementSource != null && inspectedElementID !== null) {
|
||||
viewElementSource(inspectedElementID);
|
||||
}
|
||||
}, [selectedElementID, viewElementSource]);
|
||||
}, [inspectedElementID, viewElementSource]);
|
||||
|
||||
if (element === null) {
|
||||
return (
|
||||
@@ -267,80 +265,3 @@ function OwnerView({ displayName, id }: { displayName: string, id: number }) {
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function hydrateHelper(dehydratedData: DehydratedData | null): Object | null {
|
||||
if (dehydratedData !== null) {
|
||||
return hydrate(dehydratedData.data, dehydratedData.cleaned);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function useInspectedElement(id: number | null): InspectedElement | null {
|
||||
const idRef = useRef(id);
|
||||
const bridge = useContext(BridgeContext);
|
||||
const store = useContext(StoreContext);
|
||||
|
||||
const [inspectedElement, setInspectedElement] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Track the current selected element ID.
|
||||
// We ignore any backend updates about previously selected elements.
|
||||
idRef.current = id;
|
||||
|
||||
// Hide previous/stale insepected element to avoid temporarily showing the wrong values.
|
||||
setInspectedElement(null);
|
||||
|
||||
// A null id indicates that there's nothing currently selected in the tree.
|
||||
if (id === null) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const rendererID = store.getRendererIDForElement(id);
|
||||
|
||||
// Update the $r variable.
|
||||
bridge.send('selectElement', { id, rendererID });
|
||||
|
||||
// Update props, state, and context in the side panel.
|
||||
const sendBridgeRequest = () => {
|
||||
bridge.send('inspectElement', { id, rendererID });
|
||||
};
|
||||
|
||||
let timeoutID = null;
|
||||
|
||||
const onInspectedElement = (inspectedElement: InspectedElement) => {
|
||||
if (!inspectedElement || inspectedElement.id !== idRef.current) {
|
||||
// Ignore bridge updates about previously selected elements.
|
||||
return;
|
||||
}
|
||||
|
||||
if (inspectedElement !== null) {
|
||||
inspectedElement.context = hydrateHelper(inspectedElement.context);
|
||||
inspectedElement.hooks = hydrateHelper(inspectedElement.hooks);
|
||||
inspectedElement.props = hydrateHelper(inspectedElement.props);
|
||||
inspectedElement.state = hydrateHelper(inspectedElement.state);
|
||||
}
|
||||
|
||||
setInspectedElement(inspectedElement);
|
||||
|
||||
// Ask for an update in a second.
|
||||
// Make sure we only ask once though.
|
||||
clearTimeout(((timeoutID: any): TimeoutID));
|
||||
timeoutID = setTimeout(sendBridgeRequest, 1000);
|
||||
};
|
||||
|
||||
bridge.addListener('inspectedElement', onInspectedElement);
|
||||
|
||||
sendBridgeRequest();
|
||||
|
||||
return () => {
|
||||
bridge.removeListener('inspectedElement', onInspectedElement);
|
||||
|
||||
if (timeoutID !== null) {
|
||||
clearTimeout(timeoutID);
|
||||
}
|
||||
};
|
||||
}, [bridge, id, idRef, store]);
|
||||
|
||||
return inspectedElement;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
@@ -26,6 +27,11 @@ import React, {
|
||||
useReducer,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import {
|
||||
unstable_next as next,
|
||||
unstable_runWithPriority as runWithPriority,
|
||||
unstable_UserBlockingPriority as UserBlockingPriority,
|
||||
} from 'scheduler';
|
||||
import { createRegExp } from '../utils';
|
||||
import { BridgeContext, StoreContext } from '../context';
|
||||
import Store from '../../store';
|
||||
@@ -48,6 +54,9 @@ type StateContext = {|
|
||||
ownerFlatTree: Array<number> | null,
|
||||
ownerStack: Array<number>,
|
||||
ownerStackIndex: number | null,
|
||||
|
||||
// Inspection element panel
|
||||
inspectedElementID: number | null,
|
||||
|};
|
||||
|
||||
type ACTION_GO_TO_NEXT_SEARCH_RESULT = {|
|
||||
@@ -91,6 +100,9 @@ type ACTION_SET_SEARCH_TEXT = {|
|
||||
type: 'SET_SEARCH_TEXT',
|
||||
payload: string,
|
||||
|};
|
||||
type ACTION_UPDATE_INSPECTED_ELEMENT_ID = {|
|
||||
type: 'UPDATE_INSPECTED_ELEMENT_ID',
|
||||
|};
|
||||
|
||||
type Action =
|
||||
| ACTION_GO_TO_NEXT_SEARCH_RESULT
|
||||
@@ -104,7 +116,8 @@ type Action =
|
||||
| ACTION_SELECT_PARENT_ELEMENT_IN_TREE
|
||||
| ACTION_SELECT_PREVIOUS_ELEMENT_IN_TREE
|
||||
| ACTION_SELECT_OWNER
|
||||
| ACTION_SET_SEARCH_TEXT;
|
||||
| ACTION_SET_SEARCH_TEXT
|
||||
| ACTION_UPDATE_INSPECTED_ELEMENT_ID;
|
||||
|
||||
type DispatcherContext = (action: Action) => void;
|
||||
|
||||
@@ -134,6 +147,9 @@ type State = {|
|
||||
ownerStack: Array<number>,
|
||||
ownerStackIndex: number | null,
|
||||
ownerFlatTree: Array<number> | null,
|
||||
|
||||
// Inspection element panel
|
||||
inspectedElementID: number | null,
|
||||
|};
|
||||
|
||||
function reduceTreeState(store: Store, state: State, action: Action): State {
|
||||
@@ -591,6 +607,24 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
|
||||
};
|
||||
}
|
||||
|
||||
function reduceSuspenseState(
|
||||
store: Store,
|
||||
state: State,
|
||||
action: Action
|
||||
): State {
|
||||
const { type } = action;
|
||||
switch (type) {
|
||||
case 'UPDATE_INSPECTED_ELEMENT_ID':
|
||||
return {
|
||||
...state,
|
||||
inspectedElementID: state.selectedElementID,
|
||||
};
|
||||
default:
|
||||
// React can bailout of no-op updates.
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
type Props = {| children: React$Node |};
|
||||
|
||||
// TODO Remove TreeContextController wrapper element once global ConsearchText.write API exists.
|
||||
@@ -618,10 +652,12 @@ function TreeContextController({ children }: Props) {
|
||||
case 'SELECT_PARENT_ELEMENT_IN_TREE':
|
||||
case 'SELECT_PREVIOUS_ELEMENT_IN_TREE':
|
||||
case 'SELECT_OWNER':
|
||||
case 'UPDATE_INSPECTED_ELEMENT_ID':
|
||||
case 'SET_SEARCH_TEXT':
|
||||
state = reduceTreeState(store, state, action);
|
||||
state = reduceSearchState(store, state, action);
|
||||
state = reduceOwnersState(store, state, action);
|
||||
state = reduceSuspenseState(store, state, action);
|
||||
|
||||
// If the selected ID is in a collapsed subtree, reset the selected index to null.
|
||||
// We'll know the correct index after the layout effect will toggle the tree,
|
||||
@@ -660,15 +696,29 @@ function TreeContextController({ children }: Props) {
|
||||
ownerStack: [],
|
||||
ownerStackIndex: null,
|
||||
ownerFlatTree: null,
|
||||
|
||||
// Inspection element panel
|
||||
inspectedElementID: null,
|
||||
});
|
||||
|
||||
const dispatchWrapper = useCallback(
|
||||
(action: Action) => {
|
||||
// Run the first update at "user-blocking" priority in case dispatch is called from a non-React event.
|
||||
// In this case, the current (and "next") priorities would both be "normal",
|
||||
// and suspense would potentially block both updates.
|
||||
runWithPriority(UserBlockingPriority, () => dispatch(action));
|
||||
next(() => dispatch({ type: 'UPDATE_INSPECTED_ELEMENT_ID' }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
// Listen for host element selections.
|
||||
useEffect(() => {
|
||||
const handleSelectFiber = (id: number) =>
|
||||
dispatch({ type: 'SELECT_ELEMENT_BY_ID', payload: id });
|
||||
dispatchWrapper({ type: 'SELECT_ELEMENT_BY_ID', payload: id });
|
||||
bridge.addListener('selectFiber', handleSelectFiber);
|
||||
return () => bridge.removeListener('selectFiber', handleSelectFiber);
|
||||
}, [bridge, dispatch]);
|
||||
}, [bridge, dispatchWrapper]);
|
||||
|
||||
// If a newly-selected search result or inspection selection is inside of a collapsed subtree, auto expand it.
|
||||
// This needs to be a layout effect to avoid temporarily flashing an incorrect selection.
|
||||
@@ -692,7 +742,7 @@ function TreeContextController({ children }: Props) {
|
||||
addedElementIDs,
|
||||
removedElementIDs,
|
||||
]: Array<Uint32Array>) => {
|
||||
dispatch({
|
||||
dispatchWrapper({
|
||||
type: 'HANDLE_STORE_MUTATION',
|
||||
payload: [addedElementIDs, removedElementIDs],
|
||||
});
|
||||
@@ -703,7 +753,7 @@ function TreeContextController({ children }: Props) {
|
||||
// At the moment, we can treat this as a mutation.
|
||||
// We don't know which Elements were newly added/removed, but that should be okay in this case.
|
||||
// It would only impact the search state, which is unlikely to exist yet at this point.
|
||||
dispatch({
|
||||
dispatchWrapper({
|
||||
type: 'HANDLE_STORE_MUTATION',
|
||||
payload: [new Uint32Array(0), new Uint32Array(0)],
|
||||
});
|
||||
@@ -712,11 +762,11 @@ function TreeContextController({ children }: Props) {
|
||||
store.addListener('mutated', handleStoreMutated);
|
||||
|
||||
return () => store.removeListener('mutated', handleStoreMutated);
|
||||
}, [dispatch, initialRevision, store]);
|
||||
}, [dispatchWrapper, initialRevision, store]);
|
||||
|
||||
return (
|
||||
<TreeStateContext.Provider value={state}>
|
||||
<TreeDispatcherContext.Provider value={dispatch}>
|
||||
<TreeDispatcherContext.Provider value={dispatchWrapper}>
|
||||
{children}
|
||||
</TreeDispatcherContext.Provider>
|
||||
</TreeStateContext.Provider>
|
||||
|
||||
@@ -56,7 +56,7 @@ export function useLocalStorage<T>(
|
||||
value => {
|
||||
try {
|
||||
const valueToStore =
|
||||
value instanceof Function ? value(storedValue) : value;
|
||||
value instanceof Function ? (value: any)(storedValue) : value;
|
||||
setStoredValue(valueToStore);
|
||||
window.localStorage.setItem(key, JSON.stringify(valueToStore));
|
||||
} catch (error) {
|
||||
|
||||
@@ -5126,10 +5126,10 @@ flatstr@^1.0.9:
|
||||
resolved "https://registry.yarnpkg.com/flatstr/-/flatstr-1.0.9.tgz#0950d56fec02de1030c1311847ecd58c25690eb9"
|
||||
integrity sha512-qFlJnOBWDfIaunF54/lBqNKmXOI0HqNhu+mHkLmbaBXlS71PUd9OjFOdyevHt/aHoHB1+eW7eKHgRKOG5aHSpw==
|
||||
|
||||
flow-bin@^0.96.0:
|
||||
version "0.96.0"
|
||||
resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.96.0.tgz#3b0379d97304dc1879ae6db627cd2d6819998661"
|
||||
integrity sha512-OSxERs0EdhVxEVCst/HmlT/RcnXsQQIRqcfK9J9wC8/93JQj+xQz4RtlsmYe1PSRYaozuDLyPS5pIA81Zwzaww==
|
||||
flow-bin@^0.97.0:
|
||||
version "0.97.0"
|
||||
resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.97.0.tgz#036ffcfc27503367a9d906ec9d843a0aa6f6bb83"
|
||||
integrity sha512-jXjD05gkatLuC4+e28frH1hZoRwr1iASP6oJr61Q64+kR4kmzaS+AdFBhYgoYS5kpoe4UzwDebWK8ETQFNh00w==
|
||||
|
||||
fluent-syntax@0.10.0:
|
||||
version "0.10.0"
|
||||
@@ -9912,14 +9912,6 @@ scheduler@0.0.0-4221565e1:
|
||||
loose-envify "^1.1.0"
|
||||
object-assign "^4.1.1"
|
||||
|
||||
scheduler@^0.14.0-alpha.0:
|
||||
version "0.14.0-alpha.0"
|
||||
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.14.0-alpha.0.tgz#6d301d8fd10373487e0e47e837ad24e863ce807d"
|
||||
integrity sha512-qlVVnhIJqLu9E09ZYYJHG4nYwrS4TG2zfODfS/Mk3oSGjeJlrBZTFSU3yfMdu5lIrYzWmHHWFgERPsKbildS6Q==
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
object-assign "^4.1.1"
|
||||
|
||||
schema-utils@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770"
|
||||
|
||||
Reference in New Issue
Block a user