Skip to content

[Feature] Bidirectional URL query params sync for list block filters #764

@michnowak

Description

@michnowak

Is your feature request related to a problem? Please describe.

List blocks (ticket-list, order-list, invoice-list, product-list, notification-list) manage filter state locally via useState + Formik. When a user applies filters, the URL does not update — meaning filtered views cannot be shared, bookmarked, or linked to from other parts of the application (e.g. clickable status boxes navigating to a pre-filtered list).

Describe the solution you'd like

Implement bidirectional synchronization between filter state and URL query parameters across all list blocks.

Key requirements:

  • New useUrlFilters hook in packages/ui/src/hooks/ that replaces useState(initialFilters) in list blocks
  • Serialization utilities for converting filter state to/from URL query params
  • Namespace-per-block prefixing ({ns}_key=value) to avoid collisions when multiple blocks appear on the same page
  • Repeated keys for multi-select values (e.g. status=open&status=pending)
  • Pagination as {ns}_page=N (1-based, human-readable)
  • View mode in URL ({ns}_view=grid), omitted when default (list)
  • Only non-default filter values written to URL (clean URLs)
  • router.replace for filter changes (no browser history pollution)
  • Dependency injection pattern — hook accepts searchParams and onUrlChange as arguments (no direct next/navigation imports), making it framework-agnostic

Blocks to migrate:

  1. ticket-list (proof of concept)
  2. order-list
  3. invoice-list
  4. product-list
  5. notification-list

Benefits:

  • Shareable/bookmarkable filtered views
  • Clickable UI elements (e.g. status summary boxes) can navigate to pre-filtered lists
  • Backward compatible — blocks without the hook continue working as before

Additional context (if applicable)

Implementation approach

New hook: useUrlFilters (packages/ui/src/hooks/use-url-filters.ts)

Replaces useState(initialFilters) in list blocks. Uses dependency injection — accepts searchParams and onUrlChange as arguments instead of importing from next/navigation directly:

interface UseUrlFiltersOptions<TFilters extends Record<string, string | number | string[]>> {
    initialFilters: TFilters;
    namespace?: string;         // prefix for URL params, e.g. 'ticket'
    excludeKeys?: (keyof TFilters)[];
    searchParams: URLSearchParams;
    onUrlChange: (params: string) => void;
}

Serialization rules:

  • Simple values: ticket_status=open
  • Multi-value: ticket_status=open&ticket_status=pending (repeated keys)
  • Pagination: ticket_page=2 (1-based, derived from offset/limit)
  • View mode: ticket_view=grid (omitted when default list)
  • Only non-default values written to URL

New utility functions (packages/ui/src/hooks/use-url-filters.utils.ts):

  • serializeFiltersToParams() — filter state → URL query string
  • deserializeParamsToFilters() — URL query string → filter state
  • filtersToPage() / pageToOffset() — pagination helpers

Files to create

  • packages/ui/src/hooks/use-url-filters.ts — the hook
  • packages/ui/src/hooks/use-url-filters.utils.ts — serialization/deserialization utilities
  • packages/ui/src/hooks/use-url-filters.test.ts — unit tests

Files to modify

  • packages/blocks/ticket-list/src/frontend/TicketList.client.tsx
  • packages/blocks/order-list/src/frontend/OrderList.client.tsx
  • packages/blocks/invoice-list/src/frontend/InvoiceList.client.tsx
  • packages/blocks/product-list/src/frontend/ProductList.client.tsx
  • packages/blocks/notification-list/src/frontend/NotificationList.client.tsx

Verification checklist

  • Filter change updates URL query params
  • Pasting URL with filters applies them on mount
  • Reset clears URL params
  • Pagination reflected in URL (?ticket_page=2)
  • View mode reflected in URL (?ticket_view=grid)
  • Two blocks on the same page use separate namespaces without collisions


This repo is using Opire - what does it mean? 👇
💵 Everyone can add rewards for this issue commenting /reward 100 (replace 100 with the amount).
🕵️‍♂️ If someone starts working on this issue to earn the rewards, they can comment /try to let everyone know!
🙌 And when they open the PR, they can comment /claim #764 either in the PR description or in a PR's comment.

🪙 Also, everyone can tip any user commenting /tip 20 @michnowak (replace 20 with the amount, and @michnowak with the user to tip).

📖 If you want to learn more, check out our documentation.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

Status

Backlog

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions