Last active
February 14, 2022 19:45
-
-
Save jonsmithers/d660655d103449fb1e1ce9f4f7b4d6d6 to your computer and use it in GitHub Desktop.
these are a few of my favorite hooks
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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