mirror of
https://github.com/facebook/react.git
synced 2026-02-27 03:07:57 +00:00
303 lines
7.9 KiB
JavaScript
303 lines
7.9 KiB
JavaScript
/**
|
|
* Copyright (c) Facebook, Inc. and its 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 throttle from 'lodash.throttle';
|
|
import {
|
|
useCallback,
|
|
useEffect,
|
|
useLayoutEffect,
|
|
useReducer,
|
|
useState,
|
|
} from 'react';
|
|
import {
|
|
localStorageGetItem,
|
|
localStorageSetItem,
|
|
} from 'react-devtools-shared/src/storage';
|
|
import {sanitizeForParse, smartParse, smartStringify} from '../utils';
|
|
|
|
type ACTION_RESET = {|
|
|
type: 'RESET',
|
|
externalValue: any,
|
|
|};
|
|
type ACTION_UPDATE = {|
|
|
type: 'UPDATE',
|
|
editableValue: any,
|
|
externalValue: any,
|
|
|};
|
|
|
|
type UseEditableValueAction = ACTION_RESET | ACTION_UPDATE;
|
|
type UseEditableValueDispatch = (action: UseEditableValueAction) => void;
|
|
type UseEditableValueState = {|
|
|
editableValue: any,
|
|
externalValue: any,
|
|
hasPendingChanges: boolean,
|
|
isValid: boolean,
|
|
parsedValue: any,
|
|
|};
|
|
|
|
function useEditableValueReducer(state, action) {
|
|
switch (action.type) {
|
|
case 'RESET':
|
|
return {
|
|
...state,
|
|
editableValue: smartStringify(action.externalValue),
|
|
externalValue: action.externalValue,
|
|
hasPendingChanges: false,
|
|
isValid: true,
|
|
parsedValue: action.externalValue,
|
|
};
|
|
case 'UPDATE':
|
|
let isNewValueValid = false;
|
|
let newParsedValue;
|
|
try {
|
|
newParsedValue = smartParse(action.editableValue);
|
|
isNewValueValid = true;
|
|
} catch (error) {}
|
|
return {
|
|
...state,
|
|
editableValue: sanitizeForParse(action.editableValue),
|
|
externalValue: action.externalValue,
|
|
hasPendingChanges:
|
|
smartStringify(action.externalValue) !== action.editableValue,
|
|
isValid: isNewValueValid,
|
|
parsedValue: isNewValueValid ? newParsedValue : state.parsedValue,
|
|
};
|
|
default:
|
|
throw new Error(`Invalid action "${action.type}"`);
|
|
}
|
|
}
|
|
|
|
// Convenience hook for working with an editable value that is validated via JSON.parse.
|
|
export function useEditableValue(
|
|
externalValue: any,
|
|
): [UseEditableValueState, UseEditableValueDispatch] {
|
|
const [state, dispatch] = useReducer<
|
|
UseEditableValueState,
|
|
UseEditableValueAction,
|
|
>(useEditableValueReducer, {
|
|
editableValue: smartStringify(externalValue),
|
|
externalValue,
|
|
hasPendingChanges: false,
|
|
isValid: true,
|
|
parsedValue: externalValue,
|
|
});
|
|
if (!Object.is(state.externalValue, externalValue)) {
|
|
if (!state.hasPendingChanges) {
|
|
dispatch({
|
|
type: 'RESET',
|
|
externalValue,
|
|
});
|
|
} else {
|
|
dispatch({
|
|
type: 'UPDATE',
|
|
editableValue: state.editableValue,
|
|
externalValue,
|
|
});
|
|
}
|
|
}
|
|
|
|
return [state, dispatch];
|
|
}
|
|
|
|
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;
|
|
}
|