پروایدر فیلتر (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
چه زمانی استفاده کنیم:
- داشبوردهایی که فیلترهای کاربر در URL باشند تا کپی-paste-شدنی باشند
- کاربر بتواند filter presetهای خود را save/load/rename کند
- چندین کامپوننت همزمان نیاز به state فیلتر مشترک دارند (سایدبار + DataTable + chart)
چه زمانی استفاده نکنیم:
- یک input سادهی filter که فقط یک بخش از یک صفحه را تأثیر میدهد → از
useStateمعمولی استفاده کنید - اپ شما server-state library سنگین (TanStack Query) دارد که خودش URL را sync میکند → آن را قبضه نکنید
استفاده
import { FilterProvider, useFilterState, useFilterParams, useFilterPresets } from '@parto-system-design/ui'
interface MyFilters {
q: string
province: string | null
page: number
}
const initial: MyFilters = { q: '', province: null, page: 1 }
function App() {
return (
<FilterProvider initialState={initial}>
<UrlSync />
<FilterControls />
<Results />
<SavedPresets />
</FilterProvider>
)
}
function UrlSync() {
useFilterParams<MyFilters>({
serialize: (s) => ({
q: s.q || undefined,
province: s.province ?? undefined,
page: s.page === 1 ? undefined : String(s.page),
}),
parse: (p) => ({
q: p.get('q') ?? '',
province: p.get('province'),
page: p.has('page') ? Number(p.get('page')) : 1,
}),
})
return null
}
function FilterControls() {
const { state, patch } = useFilterState<MyFilters>()
return <input value={state.q} onChange={(e) => patch({ q: e.target.value })} />
}
function SavedPresets() {
const presets = useFilterPresets<MyFilters>({ storageKey: 'my-app:filters' })
return (
<>
<Button 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، مقدارهای پیشفرض (page=1،q='') را undefined کنید تا URL تمیز بماند - برای edge case دادههای خراب در localStorage، hook خودکار ignore میکند — نگران throw نباشید
نکنید
useFilterParamsرا در چند جای صفحه فراخوانی نکنید — یکی کافی است (همیشه در یک کامپوننت<UrlSync />که فقط hook را call میکند) - وقتی data در حال load شدن از سرور است،disabled: trueپاس بدهید تا state همگام نشود قبل از آنکه consumer آماده باشد
کامپوننتهای مرتبط
- panel فیلتر و chipها →
FilterPanel+FilterPanelTrigger+ActiveFiltersBar - کارت preset که load میکند →
SavedQueryCard - command palette با recents →
CommandPalette