import { useCallback, useEffect, useMemo, useRef } from 'react'; import { useSearchParams } from 'react-router-dom'; /** * وضعیت لیست (جستجو، فیلترها، صفحه، تب) در query string زندگی می‌کند، نه در state * کامپوننت. دلیلش «بازگشت»‌ است: `navigate(-1)` همان URL قبلی را برمی‌گرداند، پس هر * چیزی که در URL باشد سرِ جایش برمی‌گردد و هر چیزی که در state باشد با remount می‌پرد. * سود جانبی: رفرش و اشتراک لینک هم همان نما را می‌دهد. * * مقدارِ برابرِ پیش‌فرض از URL حذف می‌شود تا آدرس تمیز بماند. */ export function useUrlState>(defaults: T) { const [params, setParams] = useSearchParams(); const state = useMemo(() => { const out = { ...defaults }; (Object.keys(defaults) as (keyof T)[]).forEach((k) => { const raw = params.get(String(k)); if (raw !== null) out[k] = raw as T[keyof T]; }); return out; }, [params, defaults]); // `setSearchParams` مثل setState صف نمی‌شود؛ دو فراخوانی در یک handler، اگر هر دو از // `params` فعلی شروع کنند، همدیگر را پاک می‌کنند. این ref آخرین مقدارِ نوشته‌شده را // نگه می‌دارد تا فراخوانی‌های پشت‌سرهم روی هم انباشته شوند. const latest = useRef(params); useEffect(() => { latest.current = params; }, [params]); /** * چند کلید را با هم می‌نویسد. `replace` پیش‌فرض است تا تایپ در فیلد جستجو * تاریخچه را پر نکند و یک بار «بازگشت» کاربر را از صفحه بیرون ببرد. */ const setState = useCallback((patch: Partial, opts?: { push?: boolean }) => { const next = new URLSearchParams(latest.current); Object.entries(patch).forEach(([k, v]) => { const value = v === undefined ? '' : String(v); if (value === '' || value === defaults[k as keyof T]) next.delete(k); else next.set(k, value); }); latest.current = next; setParams(next, { replace: !opts?.push }); }, [setParams, defaults]); return [state, setState] as const; } /** عدد صفحه از URL — همیشه ≥ ۱. */ export function pageOf(value: string): number { return Math.max(1, Number(value) || 1); }