Merge pull request #196 from bvaughn/selected-element-suspense

Convert inspected element (right panel) to use Suspense
This commit is contained in:
Brian Vaughn
2019-04-23 18:33:05 -07:00
committed by GitHub
11 changed files with 379 additions and 165 deletions
+2 -2
View File
@@ -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",
+10 -3
View File
@@ -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
View File
@@ -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;
}
+4 -3
View File
@@ -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);
}
+13 -3
View File
@@ -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 };
+23 -102
View File
@@ -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;
}
+57 -7
View File
@@ -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>
+1 -1
View File
@@ -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) {
+4 -12
View File
@@ -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"