Skip to content

Instantly share code, notes, and snippets.

@jonsmithers
Last active February 14, 2022 19:45
Show Gist options
  • Save jonsmithers/d660655d103449fb1e1ce9f4f7b4d6d6 to your computer and use it in GitHub Desktop.
Save jonsmithers/d660655d103449fb1e1ce9f4f7b4d6d6 to your computer and use it in GitHub Desktop.
these are a few of my favorite hooks
/**
* Calls the provided factory on first render, then returns THAT value on all
* renders thereafter.
*/
export function useMakeOnce<T>(factory: () => T): T {
const [value] = useState(factory);
return value;
}
/**
* Returns a weak map instance shared across all renders of this component.
* <p>
* If you don't know what WeakMap is: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Keyed_collections#WeakMap_object
*/
// eslint-disable-next-line @typescript-eslint/ban-types
export function useWeakMap<K extends object, V>(): WeakMap<K, V> {
return useMakeOnce(() => new WeakMap<K, V>());
}
/**
* @returns getter - a getter which trigger a lazy fetch when first invoked.
* The getter returns undefined until the fetch resolves. Triggers a re-render
* when the fetch resolves, so the next render is provided with the resolved
* value.
*/
export function useLazyFetch<T>(fetcher: (() => undefined | Promise<T>)): () => T | undefined {
const valueByFetcher = useWeakMap<() => undefined | Promise<T>, T>();
const promiseByFetcher = useWeakMap<() => undefined | Promise<T>, Promise<void>>();
const forceRerender = useForceRerender();
const fetcherRef = useRef<() => undefined | Promise<T>>();
fetcherRef.current = fetcher;
const getValue = useCallback(() => {
const value = valueByFetcher.get(fetcher);
if (value) {
return value;
}
if (!promiseByFetcher.get(fetcher)) {
const newPromise = fetcher();
if (!newPromise) {
return undefined;
}
promiseByFetcher.set(fetcher, newPromise.then((newValue) => {
valueByFetcher.set(fetcher, newValue);
if (fetcher === fetcherRef.current) {
forceRerender();
}
}));
}
return undefined;
}, [promiseByFetcher, valueByFetcher, forceRerender, fetcher]);
return getValue;
}
/**
* For performance debugging only.
*
* @example A
*
* useDebugDiffer('<MyComponent /> props', props);
*
* @example B
*
* useDebugDiffer('useCallback deps', [depA, depB, depC]);
*
* @param name
* @param props
*/
export function useDebugDiffer<T extends Record<string, unknown>>(name: string, props: T): void {
const oldPropsRef = useRef<T|null>(null);
if (oldPropsRef.current === null) {
console.log(name, 'first render'); // eslint-disable-line no-console
} else {
const old = oldPropsRef.current;
const keys = Array.from(new Set([
...Object.keys(props),
...Object.keys(old),
]));
const changedKeys = keys.filter(key => props[key] !== old[key]);
if (changedKeys.length) {
console.log(name, changedKeys); // eslint-disable-line no-console
} else {
console.log(name, `no changed values (instance equality: ${props === oldPropsRef.current})`); // eslint-disable-line no-console
}
}
oldPropsRef.current = props;
}
/**
* @param value - current value
* @return the value from the previous render (or undefined for first render)
*/
function usePreviousValue<T>(value: T): T | undefined {
const previousValueRef = useRef<T | undefined>(undefined);
const previousValue = previousValueRef.current;
previousValueRef.current = value;
return previousValue;
}
/**
* Returns true if this value is different this render from what it was _last_ render.
*
* ISSUE: on first render, this returns `(value === undefined)`
*
* @param value
*/
export function useHasChanged<T>(value: T): boolean {
return value !== usePreviousValue(value);
}
/**
* @param generatePromise - Invoked once on mount (or whenever changed).
* @return getter - Triggers suspense when the generated promise hasn't
* resolved. Otherwise, returns the resolved promise value.
*/
export function useFetchOnMountWithSuspense<T>(generatePromise: () => Promise<T>): () => T {
const valueMap = useWeakMap<() => Promise<T>, T>();
const promiseMap = useWeakMap<() => Promise<T>, Promise<T>>();
useEffect(() => {
const promise = generatePromise();
promiseMap.set(generatePromise, promise);
promise.then((value) => {
valueMap.set(generatePromise, value);
});
}, [generatePromise, promiseMap, valueMap]);
return useCallback(() => {
const value = valueMap.get(generatePromise);
if (value === undefined) {
throw promiseMap.get(generatePromise);
}
return value;
}, [generatePromise, promiseMap, valueMap]);
}
/**
* @param generatePromise - Invoked the first time "getter" is invoked
* @return getter - Triggers suspense when the generated promise hasn't
* resolved. Otherwise, returns the resolved promise value.
*/
export function useLazyFetchWithSuspense<T>(generatePromise: () => Promise<T>): () => T {
const valueMap = useWeakMap<() => Promise<T>, T>();
const promiseMap = useWeakMap<() => Promise<T>, Promise<T>>();
return useCallback(() => {
if (!promiseMap.has(generatePromise)) {
const promise = generatePromise();
promiseMap.set(generatePromise, promise);
promise.then((value) => {
valueMap.set(generatePromise, value);
});
}
const value = valueMap.get(generatePromise);
if (value === undefined) {
throw promiseMap.get(generatePromise);
}
return value;
}, [generatePromise, promiseMap, valueMap]);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment