diff --git a/package.json b/package.json index 44549789ea..c8f49c3d16 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/devtools/ProfilingCache.js b/src/devtools/ProfilingCache.js index e0b4cfabc9..d313111e67 100644 --- a/src/devtools/ProfilingCache.js +++ b/src/devtools/ProfilingCache.js @@ -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(); diff --git a/src/devtools/cache.js b/src/devtools/cache.js index 58577adf32..1e12798030 100644 --- a/src/devtools/cache.js +++ b/src/devtools/cache.js @@ -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 = { +export type Thenable = { then(resolve: (T) => mixed, reject: (mixed) => mixed): mixed, }; @@ -25,9 +25,9 @@ type PendingResult = {| value: Suspender, |}; -type ResolvedResult = {| +type ResolvedResult = {| status: 1, - value: V, + value: Value, |}; type RejectedResult = {| @@ -35,11 +35,14 @@ type RejectedResult = {| value: mixed, |}; -type Result = PendingResult | ResolvedResult | RejectedResult; +type Result = PendingResult | ResolvedResult | RejectedResult; -export type Resource = { - read(I): V, - preload(I): void, +export type Resource = { + 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, Map> = new Map(); +type Config = { + useWeakMap?: boolean, +}; -function accessResult( - resource: any, - fetch: I => Thenable, - input: I, - key: K -): Result { - let entriesForResource = entries.get(resource); +const entries: Map< + Resource, + Map | WeakMap +> = new Map(); +const resourceConfigs: Map, Config> = new Map(); + +function getEntriesForResource( + resource: any +): Map | WeakMap { + let entriesForResource = ((entries.get(resource): any): Map); 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( + resource: any, + fetch: Input => Thenable, + input: Input, + key: Key +): Result { + 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 = (newResult: any); + const resolvedResult: ResolvedResult = (newResult: any); resolvedResult.status = Resolved; resolvedResult.value = value; } @@ -110,21 +126,28 @@ function accessResult( } } -export function createResource( - fetch: I => Thenable, - maybeHashInput?: I => K -): Resource { - const hashInput: I => K = - maybeHashInput !== undefined ? maybeHashInput : (identityHashFn: any); - +export function createResource( + fetch: Input => Thenable, + hashInput: Input => Key, + config?: Config = {} +): Resource { 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 = accessResult(resource, fetch, input, key); + const result: Result = accessResult(resource, fetch, input, key); switch (result.status) { case Pending: { const suspender = result.value; @@ -144,7 +167,7 @@ export function createResource( } }, - 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( const key = hashInput(input); accessResult(resource, fetch, input, key); }, + + write(key: Key, value: Value): void { + const entriesForResource = getEntriesForResource(resource); + + const resolvedResult: ResolvedResult = { + status: Resolved, + value, + }; + + entriesForResource.set(key, resolvedResult); + }, }; + + resourceConfigs.set(resource, config); + return resource; } diff --git a/src/devtools/store.js b/src/devtools/store.js index a59c9c9fa4..63d4ce543b 100644 --- a/src/devtools/store.js +++ b/src/devtools/store.js @@ -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 = 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. diff --git a/src/devtools/views/Components/Components.css b/src/devtools/views/Components/Components.css index 6523b5e515..775b1faef1 100644 --- a/src/devtools/views/Components/Components.css +++ b/src/devtools/views/Components/Components.css @@ -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); +} diff --git a/src/devtools/views/Components/Components.js b/src/devtools/views/Components/Components.js index f38830baab..bce5941af0 100644 --- a/src/devtools/views/Components/Components.js +++ b/src/devtools/views/Components/Components.js @@ -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(_: {||}) {
- + + }> + + +
); } +function Loading() { + return
Loading...
; +} + export default portaledContent(Components); diff --git a/src/devtools/views/Components/InspectedElementContext.js b/src/devtools/views/Components/InspectedElementContext.js new file mode 100644 index 0000000000..0a87518f05 --- /dev/null +++ b/src/devtools/views/Components/InspectedElementContext.js @@ -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(((null: any): Context)); +InspectedElementContext.displayName = 'InspectedElementContext'; + +type ResolveFn = (inspectedElement: InspectedElement) => void; +type InProgressRequest = {| + promise: Thenable, + resolveFn: ResolveFn, +|}; + +const inProgressRequests: WeakMap = new WeakMap(); +const resource: Resource = 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(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 ( + + {children} + + ); +} + +function hydrateHelper(dehydratedData: DehydratedData | null): Object | null { + if (dehydratedData !== null) { + return hydrate(dehydratedData.data, dehydratedData.cleaned); + } else { + return null; + } +} + +export { InspectedElementContext, InspectedElementContextController }; diff --git a/src/devtools/views/Components/SelectedElement.js b/src/devtools/views/Components/SelectedElement.js index cf4a7c5f33..c2b62a6389 100644 --- a/src/devtools/views/Components/SelectedElement.js +++ b/src/devtools/views/Components/SelectedElement.js @@ -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 }) { ); } - -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; -} diff --git a/src/devtools/views/Components/TreeContext.js b/src/devtools/views/Components/TreeContext.js index 85e4b8cd4e..21b16e89a0 100644 --- a/src/devtools/views/Components/TreeContext.js +++ b/src/devtools/views/Components/TreeContext.js @@ -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 | null, ownerStack: Array, 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, ownerStackIndex: number | null, ownerFlatTree: Array | 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) => { - 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 ( - + {children} diff --git a/src/devtools/views/hooks.js b/src/devtools/views/hooks.js index 3b0c064ba0..6d48915d20 100644 --- a/src/devtools/views/hooks.js +++ b/src/devtools/views/hooks.js @@ -56,7 +56,7 @@ export function useLocalStorage( 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) { diff --git a/yarn.lock b/yarn.lock index 712643e1d5..557d169899 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"