پرتوپرتو

پروایدر فیلتر (FilterProvider)

لایه‌ی state-management برای FilterPanel — context provider، sync با URL، ذخیره‌سازی preset در localStorage

معرفی

FilterPanel خود state-agnostic است: شما باید state، sync با URL، و saved presets را خودتان مدیریت کنید. این سه قطعه کنار هم آن کار را برای شما می‌کنند:

  1. <FilterProvider> + useFilterState<T>() — Context provider با حالت reset/patch/set
  2. useFilterParams<T>() — sync دو-طرفه با URLSearchParams (debounced + Persian-digit normalize)
  3. 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 target
  • state?: 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 merge
  • reset() — بازگشت به 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 — پیش‌فرض 200
  • history?: 'push' | 'replace' — پیش‌فرض replace (back-button useful می‌ماند)
  • disabled?: boolean — برای حالت‌های suspended

Persian/Arabic digits در URL خودکار به Latin normalize می‌شوند.

useFilterPresets<T>({ storageKey, maxPresets? })

  • presets: FilterPreset<T>[] — newest-first
  • save(name) → id
  • load(id) → boolean
  • remove(id)
  • rename(id, name) → boolean
  • overwrite(id) → boolean — جایگزینی state preset با state جاری
  • clear()

پیش‌فرض maxPresets=20 — سرریز قدیمی‌ترین حذف می‌شود.

جدول ویژگی‌ها

FilterProvider

Prop

Type

useFilterState<T>()

Prop

Type

useFilterParams<T>(options)

Prop

Type

useFilterPresets<T>(options)

Prop

Type

مقادیر بازگشتی: { 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 موقعیت خود را گم نکنند.
  • وقتی useFilterParams URL را به‌روزرسانی می‌کند، از 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 با recentsCommandPalette