پروایدر فیلتر (FilterProvider)
لایهی state-management برای FilterPanel — context provider، sync با URL، ذخیرهسازی preset در localStorage؛ نه برای صفحهٔ فهرست یا داشبوردی که با قالب ساخته میشود
معرفی
FilterPanel خود state-agnostic است: شما باید state، sync با URL، و saved presets را خودتان مدیریت کنید. این سه قطعه کنار هم آن کار را برای شما میکنند:
<FilterProvider>+useFilterState<T>()— Context provider با حالت reset/patch/setuseFilterParams<T>()— sync دو-طرفه باURLSearchParams(debounced + Persian-digit normalize)useFilterPresets<T>()— CRUD ذخیره/بازخوانی preset در localStorage
نه برای صفحهٔ فهرست یا داشبوردی که با قالب ساخته میشود
یک ListPage یا DashboardPage جستوجو، فیلترها،
بازه و شمارهٔ صفحهاش را با React.useState در همان کامپوننتی نگه میدارد که قالب را رندر میکند و به propهای قالب
میدهد — نه FilterProvider، و هرگز در نشانی صفحه (useFilterParams، useSearchParams). یک پاسخ برای همهٔ
صفحهها (انتخاب قالب صفحه)؛ قاعدهٔ ESLint parto/page-template اینها را در فایلی
که یکی از این دو قالب را دارد نشان میدهد.
چه زمانی استفاده کنیم:
- یک
FilterPanelدرون محتوای یکCustomPageکه چند جزء دورِ هم state فیلتر مشترک لازم دارند (پنل + چیپها + محتوایی که فیلتر میشود) - کاربر بتواند filter presetهای آن پنل را save/load/rename کند
- (با
useFilterParams) آن پنل باید با لینک به اشتراک گذاشته شود
چه زمانی استفاده نکنیم:
- صفحهٔ فهرست یا داشبورد —
ListPage/DashboardPageباReact.useState(بالا) - یک input سادهی filter که فقط یک بخش از یک صفحه را تأثیر میدهد → از
useStateمعمولی استفاده کنید - اپ شما server-state library سنگین (TanStack Query) دارد که خودش URL را sync میکند → آن را قبضه نکنید
- تأخیر ارسال سفارشهای کمپین پاییزیتهران · ارسال
- مقایسهٔ قیمت با فروشگاههای دیگراصفهان · قیمت
- ارسال رایگان برای شهرهای کوچکاصفهان · ارسال
- کد تخفیف فصلی کار نمیکندتهران · قیمت
- بازخورد بستهبندی تازهفارس · بستهبندی
استفاده
import {
Button,
FilterProvider,
SavedQueryCard,
useFilterState,
useFilterParams,
useFilterPresets,
} from '@partodata/ui'
// A filter panel's state inside a CustomPage (a map of conversations) — not a ListPage's or a DashboardPage's.
// A type (not an interface): the provider's state is a Record<string, unknown>.
type MapFilters = {
province: string | null
topic: string | null
}
const initial: MapFilters = { province: null, topic: null }
function ConversationMap() {
return (
<FilterProvider initialState={initial}>
<UrlSync />
<FilterControls />
<MapResults />
<SavedPresets />
</FilterProvider>
)
}
function UrlSync() {
useFilterParams<MapFilters>({
serialize: (s) => ({ province: s.province ?? undefined, topic: s.topic ?? undefined }),
parse: (p) => ({ province: p.get('province'), topic: p.get('topic') }),
})
return null
}
function FilterControls() {
const { state, patch } = useFilterState<MapFilters>()
return (
<Button variant="default" onClick={() => patch({ province: state.province ? null : 'tehran' })}>
{state.province ? 'همهٔ استانها' : 'فقط تهران'}
</Button>
)
}
function MapResults() {
const { state } = useFilterState<MapFilters>()
return <p className="text-sm text-foreground-light">{state.topic ?? 'همهٔ موضوعها'}</p>
}
function SavedPresets() {
const presets = useFilterPresets<MapFilters>({ storageKey: 'conversation-map:filters' })
return (
<>
<Button variant="default" onClick={() => presets.save('گزارش هفتگی')}>
ذخیره
</Button>
{presets.presets.map((p) => (
<SavedQueryCard key={p.id} name={p.name} onRun={() => presets.load(p.id)} />
))}
</>
)
}API
FilterProvider
initialState: T— حالت اولیهی reset targetstate?: T— controlled mode (تنها وقتی consumer state را خارج از provider نگه میدارد)onStateChange?: (next: T) => void— هر تغییر state
useFilterState<T>()
const { state, set, patch, reset } = useFilterState<T>()state— حالت جاریset(next)— جایگزینی کاملpatch(partial)— shallow mergereset()— بازگشت بهinitialStateکه در زمان mount اول capture شد
useFilterParams<T>(options)
serialize(state) → Record<string, string | undefined | null>— فقط مقادیر non-null/non-empty در URL مینشینندparse(URLSearchParams) → Partial<T>— روی mount + popstate صدا زده میشودdebounceMs?: number— پیشفرض 200history?: 'push' | 'replace'— پیشفرضreplace(back-button useful میماند)disabled?: boolean— برای حالتهای suspended
Persian/Arabic digits در URL خودکار به Latin normalize میشوند.
useFilterPresets<T>({ storageKey, maxPresets? })
presets: FilterPreset<T>[]— newest-firstsave(name) → idload(id) → booleanremove(id)rename(id, name) → booleanoverwrite(id) → boolean— جایگزینی state preset با state جاریclear()
پیشفرض maxPresets=20 — سرریز قدیمیترین حذف میشود.
جدول ویژگیها
FilterProvider
useFilterState<T>()
useFilterParams<T>(options)
useFilterPresets<T>(options)
مقادیر بازگشتی: { presets, save(name) → id, load(id) → boolean, remove(id), rename(id, name) → boolean, overwrite(id) → boolean, clear() }
دسترسیپذیری
این کامپوننت ارائهدهندهی Context است و رابط بصری ندارد — دسترسیپذیری از عهدهی فرزندان (FilterPanel، SavedQueryCard، …) است. نکات مرتبط هنگام wiring:
- اطمینان حاصل کنید پس از
reset()فوکوس روی dispatch کنندهی reset باقی بماند تا کاربران keyboard موقعیت خود را گم نکنند. - وقتی
useFilterParamsURL را بهروزرسانی میکند، ازhistory: 'replace'(پیشفرض) استفاده کنید تا back-button دکمههای قبلی navigation را نشکند. - در صفحهی preset، هنگام
load(id)یکaria-live="polite"متن «فیلتر بارگذاری شد» نمایش دهید تا screen reader اعلام کند state تغییر کرده.
راهنمای استفاده
بکنید
- برای هر app/section یک
storageKeyیکتا انتخاب کنید (comments-app:filters،bulletins:filters) - درserialize، مقدارهای پیشفرض پنل (province: null) را undefined کنید تا URL تمیز بماند - برای edge case دادههای خراب در localStorage، hook خودکار ignore میکند — نگران throw نباشید
نکنید
useFilterParamsرا در چند جای صفحه فراخوانی نکنید — یکی کافی است (همیشه در یک کامپوننت<UrlSync />که فقط hook را call میکند) - وقتی data در حال load شدن از سرور است،disabled: trueپاس بدهید تا state همگام نشود قبل از آنکه consumer آماده باشد
کامپوننتهای مرتبط
- سایدبار فیلتر و چیپهایش →
filterPanelقالب (FilterPanel؛ چیپها باactiveFiltersداخل سایدبار،FilterPanelActiveFilters) - کارت preset که load میکند →
SavedQueryCard - command palette با recents →
CommandPalette