پروایدر فیلتر (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

نه برای صفحهٔ فهرست یا داشبوردی که با قالب ساخته می‌شود

یک 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 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، مقدارهای پیش‌فرض پنل (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