You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
bazarr/frontend/src/utilities/hooks.ts

165 lines
3.6 KiB

import { SelectorOption, SelectorProps } from "@/components";
import { SliderProps } from "@mantine/core";
import {
Dispatch,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { useNavigate } from "react-router-dom";
export function useGotoHomepage() {
const navigate = useNavigate();
return useCallback(() => navigate("/"), [navigate]);
}
export function useSelectorOptions<T>(
options: readonly T[],
label: (value: T) => string,
key?: (value: T) => string
): Pick<SelectorProps<T>, "options" | "getkey"> {
const labelRef = useRef(label);
labelRef.current = label;
const keyRef = useRef(key);
keyRef.current = key;
const wrappedOptions = useMemo(
() =>
options.map<SelectorOption<T>>((value) => ({
value,
label: labelRef.current(value),
})),
[options]
);
return useMemo(
() => ({
options: wrappedOptions,
getkey: keyRef.current ?? labelRef.current,
}),
[wrappedOptions]
);
}
export function useSliderMarks(values: number[]): SliderProps["marks"] {
return useMemo<SliderProps["marks"]>(
() =>
values.map((value) => ({
value: value,
label: value.toString(),
})),
[values]
);
}
// High performance action wrapper for array, typically used for table updates
export function useArrayAction<T>(setData: Dispatch<(prev: T[]) => T[]>) {
const setDataRef = useRef(setData);
setDataRef.current = setData;
const add = useCallback((row: T) => {
setDataRef.current((data) => {
return [...data, row];
});
}, []);
const mutate = useCallback((index: number, row: T) => {
setDataRef.current((data) => {
if (index !== -1) {
const list = [...data];
list[index] = row;
return list;
}
return data;
});
}, []);
const remove = useCallback((index: number) => {
setDataRef.current((data) => {
if (index !== -1) {
const list = [...data];
list.splice(index, 1);
return list;
}
return data;
});
}, []);
const update = useCallback((fn: (item: T) => T) => {
setDataRef.current((data) => {
return data.map(fn);
});
}, []);
return useMemo(
() => ({
add,
mutate,
remove,
update,
}),
[add, mutate, remove, update]
);
}
export function useThrottle<F extends GenericFunction>(fn: F, ms: number) {
const fnRef = useRef(fn);
fnRef.current = fn;
const timer = useRef<number>();
return useCallback(
(...args: Parameters<F>) => {
if (timer.current) {
clearTimeout(timer.current);
timer.current = undefined;
}
timer.current = window.setTimeout(() => fnRef.current(...args), ms);
},
[ms]
);
}
export function useDebouncedValue<T>(item: T, ms: number) {
const [value, setValue] = useState(item);
const debouncedSetValue = useThrottle(setValue, ms);
useEffect(() => {
debouncedSetValue(item);
}, [debouncedSetValue, item]);
return value;
}
export function useOnValueChange<T>(value: T, onChange: (value: T) => void) {
const valueRef = useRef<T | null>(null);
const onChangeRef = useRef(onChange);
onChangeRef.current = onChange;
useEffect(() => {
if (valueRef.current !== value) {
valueRef.current = value;
onChangeRef.current(value);
}
}, [value]);
}
// Mantine's useInterval has some weird issues. This is a workaround.
export function useInterval(fn: VoidFunction, ms: number) {
const timer = useRef<number>();
useEffect(() => {
timer.current = window.setInterval(fn, ms);
return () => {
clearInterval(timer.current);
};
}, [fn, ms]);
}