addons/shared/hooks.tsxblame
View source
b69ab311/**
b69ab312 * Copyright (c) Meta Platforms, Inc. and affiliates.
b69ab313 *
b69ab314 * This source code is licensed under the MIT license found in the
b69ab315 * LICENSE file in the root directory of this source tree.
b69ab316 */
b69ab317
b69ab318import deepEqual from 'fast-deep-equal';
b69ab319import {useCallback, useEffect, useMemo, useRef} from 'react';
b69ab3110import {debounce} from './debounce';
b69ab3111
b69ab3112/**
b69ab3113 * Like useEffect, but throttles calls to the effect callback.
b69ab3114 * This can help avoid overfiring effects that need to happen during render.
b69ab3115 *
b69ab3116 * Note: Do not use this just to bypass effects firing twice
b69ab3117 * in strict + dev mode. Double-firing is done to help detect bugs.
b69ab3118 * Throttling is not suitable for subscriptions that must stay in sync
b69ab3119 * or queries which need to stay in sync as things update.
b69ab3120 *
b69ab3121 * This is most useful for best-effort side-effects like logging & analytics
b69ab3122 * which don't require exact synchronization and don't affect UI state.
b69ab3123 */
b69ab3124export function useThrottledEffect<A extends Array<unknown>>(
b69ab3125 cb: (...args: A) => void,
b69ab3126 throttleTimeMs: number,
b69ab3127 deps?: Array<unknown>,
b69ab3128): void {
b69ab3129 // eslint-disable-next-line react-hooks/exhaustive-deps
b69ab3130 const throttled = useCallback(debounce(cb, throttleTimeMs, undefined, true), [
b69ab3131 throttleTimeMs,
b69ab3132 ...(deps ?? []),
b69ab3133 ]);
b69ab3134 return useEffect((...args: A) => {
b69ab3135 return throttled(...args);
b69ab3136 // eslint-disable-next-line react-hooks/exhaustive-deps
b69ab3137 }, deps);
b69ab3138}
b69ab3139
b69ab3140/**
b69ab3141 * Like React.useMemo, but with deep equality comparison between previous/next dependencies.
b69ab3142 */
b69ab3143export function useDeepMemo<T>(construct: () => T, dependencies: React.DependencyList) {
b69ab3144 const ref = useRef<React.DependencyList>([]);
b69ab3145 if (!deepEqual(dependencies, ref.current)) {
b69ab3146 ref.current = dependencies;
b69ab3147 }
b69ab3148 const deepDeps = ref.current;
b69ab3149
b69ab3150 // eslint-disable-next-line react-hooks/exhaustive-deps
b69ab3151 return useMemo(construct, deepDeps);
b69ab3152}
b69ab3153
b69ab3154/**
b69ab3155 * Returns a react ref that you can pass to an element to autofocus it on mount.
b69ab3156 */
b69ab3157export function useAutofocusRef<T extends HTMLElement>(): React.MutableRefObject<T | null> {
b69ab3158 const ref = useRef<T | null>(null);
b69ab3159 useEffect(() => {
b69ab3160 if (ref.current != null) {
b69ab3161 ref.current.focus();
b69ab3162 }
b69ab3163 }, [ref]);
b69ab3164 return ref;
b69ab3165}
b69ab3166
b69ab3167/**
b69ab3168 * Returns the last (different) value of a given variable from a previous render.
b69ab3169 */
b69ab3170export function usePrevious<T>(value: T, equalityFn?: (a: T, b: T) => boolean): T | undefined {
b69ab3171 const ref = useRef<{value: T; prev: T | undefined}>({
b69ab3172 value,
b69ab3173 prev: undefined,
b69ab3174 });
b69ab3175
b69ab3176 const current = ref.current.value;
b69ab3177
b69ab3178 if (equalityFn != null ? !equalityFn(value, current) : value !== current) {
b69ab3179 ref.current = {
b69ab3180 value,
b69ab3181 prev: current,
b69ab3182 };
b69ab3183 }
b69ab3184
b69ab3185 return ref.current?.prev;
b69ab3186}