blakeblackshear.frigate/web/src/hooks/use-overlay-state.tsx
Nicolas Mowen b4d82084a9
Fixes (#15465)
* Fix single event return

* Allow customizing if search is preserved for overlay state

* Remove timeout

* Cleanup

* Cleanup naming
2024-12-12 08:22:30 -06:00

136 lines
3.6 KiB
TypeScript

import { useCallback, useEffect, useMemo } from "react";
import { useLocation, useNavigate, useSearchParams } from "react-router-dom";
import { usePersistence } from "./use-persistence";
export function useOverlayState<S>(
key: string,
defaultValue: S | undefined = undefined,
preserveSearch: boolean = true,
): [S | undefined, (value: S, replace?: boolean) => void] {
const location = useLocation();
const navigate = useNavigate();
const currentLocationState = useMemo(() => location.state, [location]);
const setOverlayStateValue = useCallback(
(value: S, replace: boolean = false) => {
const newLocationState = { ...currentLocationState };
newLocationState[key] = value;
navigate(location.pathname + (preserveSearch ? location.search : ""), {
state: newLocationState,
replace,
});
},
// we know that these deps are correct
// eslint-disable-next-line react-hooks/exhaustive-deps
[key, currentLocationState, navigate],
);
const overlayStateValue = useMemo<S | undefined>(
() => location.state && location.state[key],
[location, key],
);
return [overlayStateValue ?? defaultValue, setOverlayStateValue];
}
export function usePersistedOverlayState<S extends string>(
key: string,
defaultValue: S | undefined = undefined,
): [
S | undefined,
(value: S | undefined, replace?: boolean) => void,
() => void,
] {
const location = useLocation();
const navigate = useNavigate();
const currentLocationState = useMemo(() => location.state, [location]);
// currently selected value
const overlayStateValue = useMemo<S | undefined>(
() => location.state && location.state[key],
[location, key],
);
// saved value from previous session
const [persistedValue, setPersistedValue, , deletePersistedValue] =
usePersistence<S>(key, overlayStateValue);
const setOverlayStateValue = useCallback(
(value: S | undefined, replace: boolean = false) => {
setPersistedValue(value);
const newLocationState = { ...currentLocationState };
newLocationState[key] = value;
navigate(location.pathname, { state: newLocationState, replace });
},
// we know that these deps are correct
// eslint-disable-next-line react-hooks/exhaustive-deps
[key, currentLocationState, navigate],
);
return [
overlayStateValue ?? persistedValue ?? defaultValue,
setOverlayStateValue,
deletePersistedValue,
];
}
export function useHashState<S extends string>(): [
S | undefined,
(value: S) => void,
] {
const location = useLocation();
const navigate = useNavigate();
const setHash = useCallback(
(value: S | undefined) => {
if (!value) {
navigate(location.pathname);
} else {
navigate(`${location.pathname}#${value}`, { state: location.state });
}
},
// we know that these deps are correct
// eslint-disable-next-line react-hooks/exhaustive-deps
[location, navigate],
);
const hash = useMemo(
() => location.hash.substring(1) as unknown as S,
[location.hash],
);
return [hash, setHash];
}
export function useSearchEffect(
key: string,
callback: (value: string) => boolean,
) {
const [searchParams, setSearchParams] = useSearchParams();
const param = useMemo(() => {
const param = searchParams.get(key);
if (!param) {
return undefined;
}
return [key, decodeURIComponent(param)];
}, [searchParams, key]);
useEffect(() => {
if (!param) {
return;
}
const remove = callback(param[1]);
if (remove) {
setSearchParams(undefined, { replace: true });
}
}, [param, callback, setSearchParams]);
}