Files
react/packages/react-devtools-shared/src/devtools/views/hooks.js
2019-08-13 15:59:43 -07:00

202 lines
5.5 KiB
JavaScript

// @flow
import throttle from 'lodash.throttle';
import { useCallback, useEffect, useLayoutEffect, useState } from 'react';
import { localStorageGetItem, localStorageSetItem } from 'react-devtools-shared/src/storage';
export function useIsOverflowing(
containerRef: { current: HTMLDivElement | null },
totalChildWidth: number
): boolean {
const [isOverflowing, setIsOverflowing] = useState<boolean>(false);
// It's important to use a layout effect, so that we avoid showing a flash of overflowed content.
useLayoutEffect(() => {
if (containerRef.current === null) {
return () => {};
}
const container = ((containerRef.current: any): HTMLDivElement);
const handleResize = throttle(
() => setIsOverflowing(container.clientWidth <= totalChildWidth),
100
);
handleResize();
// It's important to listen to the ownerDocument.defaultView to support the browser extension.
// Here we use portals to render individual tabs (e.g. Profiler),
// and the root document might belong to a different window.
const ownerWindow = container.ownerDocument.defaultView;
ownerWindow.addEventListener('resize', handleResize);
return () => ownerWindow.removeEventListener('resize', handleResize);
}, [containerRef, totalChildWidth]);
return isOverflowing;
}
// Forked from https://usehooks.com/useLocalStorage/
export function useLocalStorage<T>(
key: string,
initialValue: T | (() => T)
): [T, (value: T | (() => T)) => void] {
const getValueFromLocalStorage = useCallback(() => {
try {
const item = localStorageGetItem(key);
if (item != null) {
return JSON.parse(item);
}
} catch (error) {
console.log(error);
}
if (typeof initialValue === 'function') {
return ((initialValue: any): () => T)();
} else {
return initialValue;
}
}, [initialValue, key]);
const [storedValue, setStoredValue] = useState(getValueFromLocalStorage);
const setValue = useCallback(
value => {
try {
const valueToStore =
value instanceof Function ? (value: any)(storedValue) : value;
setStoredValue(valueToStore);
localStorageSetItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.log(error);
}
},
[key, storedValue]
);
// Listen for changes to this local storage value made from other windows.
// This enables the e.g. "⚛️ Elements" tab to update in response to changes from "⚛️ Settings".
useLayoutEffect(() => {
const onStorage = event => {
const newValue = getValueFromLocalStorage();
if (key === event.key && storedValue !== newValue) {
setValue(newValue);
}
};
window.addEventListener('storage', onStorage);
return () => {
window.removeEventListener('storage', onStorage);
};
}, [getValueFromLocalStorage, key, storedValue, setValue]);
return [storedValue, setValue];
}
export function useModalDismissSignal(
modalRef: { current: HTMLDivElement | null },
dismissCallback: () => void,
dismissOnClickOutside?: boolean = true
): void {
useEffect(() => {
if (modalRef.current === null) {
return () => {};
}
const handleDocumentKeyDown = ({ key }: any) => {
if (key === 'Escape') {
dismissCallback();
}
};
const handleDocumentClick = (event: any) => {
// $FlowFixMe
if (
modalRef.current !== null &&
!modalRef.current.contains(event.target)
) {
event.stopPropagation();
event.preventDefault();
dismissCallback();
}
};
// It's important to listen to the ownerDocument to support the browser extension.
// Here we use portals to render individual tabs (e.g. Profiler),
// and the root document might belong to a different window.
const ownerDocument = modalRef.current.ownerDocument;
ownerDocument.addEventListener('keydown', handleDocumentKeyDown);
if (dismissOnClickOutside) {
ownerDocument.addEventListener('click', handleDocumentClick);
}
return () => {
ownerDocument.removeEventListener('keydown', handleDocumentKeyDown);
ownerDocument.removeEventListener('click', handleDocumentClick);
};
}, [modalRef, dismissCallback, dismissOnClickOutside]);
}
// Copied from https://github.com/facebook/react/pull/15022
export function useSubscription<Value>({
getCurrentValue,
subscribe,
}: {|
getCurrentValue: () => Value,
subscribe: (callback: Function) => () => void,
|}): Value {
const [state, setState] = useState({
getCurrentValue,
subscribe,
value: getCurrentValue(),
});
if (
state.getCurrentValue !== getCurrentValue ||
state.subscribe !== subscribe
) {
setState({
getCurrentValue,
subscribe,
value: getCurrentValue(),
});
}
useEffect(() => {
let didUnsubscribe = false;
const checkForUpdates = () => {
if (didUnsubscribe) {
return;
}
setState(prevState => {
if (
prevState.getCurrentValue !== getCurrentValue ||
prevState.subscribe !== subscribe
) {
return prevState;
}
const value = getCurrentValue();
if (prevState.value === value) {
return prevState;
}
return { ...prevState, value };
});
};
const unsubscribe = subscribe(checkForUpdates);
checkForUpdates();
return () => {
didUnsubscribe = true;
unsubscribe();
};
}, [getCurrentValue, subscribe]);
return state.value;
}