Files
frontend/apps/cs/src/shared/lib/use-search-params-state.ts
T

91 lines
2.7 KiB
TypeScript

"use client";
import { useCallback, useMemo, useRef } from "react";
import { useSearchParams } from "next/navigation";
/**
* 조회 조건을 URL searchParams와 동기화하는 훅.
*
* - URL이 단일 소스 오브 트루스(SSOT)이며, 새로고침·뒤로가기 시에도 조건이 유지된다.
* - 기본값과 동일한 값은 URL에서 제거해 쿼리스트링을 깨끗하게 유지한다.
* - `window.history.replaceState` 기반 shallow 갱신으로 서버 라운드트립 없이 동작한다.
* - 숫자 기본값이 주어진 키는 자동으로 `Number` 파싱한다.
*
* @example
* const { params, setParams, resetParams } = useSearchParamsState({
* keyword: "",
* page: 0,
* size: 10,
* });
*/
export function useSearchParamsState<T extends Record<string, ParamValue>>(
defaults: T,
): UseSearchParamsStateReturn<T> {
const searchParams = useSearchParams();
const defaultsRef = useRef(defaults);
const params = useMemo(
() => parseParams(searchParams, defaultsRef.current),
[searchParams],
);
const setParams = useCallback((partial: Partial<T>) => {
const next = new URLSearchParams(window.location.search);
for (const [key, value] of Object.entries(partial)) {
if (value === undefined || value === defaultsRef.current[key]) {
next.delete(key);
} else {
next.set(key, String(value));
}
}
replaceUrl(next);
}, []);
const resetParams = useCallback(() => {
const next = new URLSearchParams(window.location.search);
for (const key of Object.keys(defaultsRef.current)) {
next.delete(key);
}
replaceUrl(next);
}, []);
return { params, setParams, resetParams };
}
function parseParams<T extends Record<string, ParamValue>>(
searchParams: ReturnType<typeof useSearchParams>,
defaults: T,
): T {
const result = { ...defaults };
for (const key of Object.keys(defaults) as (keyof T)[]) {
const raw = searchParams?.get(key as string) ?? null;
if (raw === null) continue;
if (typeof defaults[key] === "number") {
const parsed = Number(raw);
if (!Number.isNaN(parsed)) result[key] = parsed as T[keyof T];
} else {
result[key] = raw as T[keyof T];
}
}
return result;
}
function replaceUrl(searchParams: URLSearchParams) {
const qs = searchParams.toString();
const url = qs
? `${window.location.pathname}?${qs}`
: window.location.pathname;
window.history.replaceState(null, "", url);
}
type ParamValue = string | number | null;
interface UseSearchParamsStateReturn<T> {
params: T;
setParams: (partial: Partial<T>) => void;
resetParams: () => void;
}