Skip to content

URL-synced list state: filters, tabs, and pagination survive refresh, back button, and sharing #133

Description

@olliethedev

Context

Part of the Refine-inspired core simplification effort (#127 ✅, #128, #129, #130 ✅, #131, #132). Refine's useTable has syncWithLocation: filters, sorting, and pagination live in the URL query string.

Scope note: this issue covers the core primitive only (phase 1): the router contract extension and the useListState hook. Migrating plugin list pages (comments moderation, media library, blog search, cms content list, form-builder submissions, kanban filters) is tracked in #136 (phase 2).

Problem

Plugin list pages keep list state in local useState, so it is lost on refresh, invisible to the back button, and impossible to share/bookmark:

  • comments/.../moderation-page.internal.tsxactiveTab, currentPage in useState
  • media/.../library-page.internal.tsxselectedFolder in useState

Concrete defects: refreshing the moderation page drops you back to "pending" tab page 1; you cannot link a teammate to "spam tab, page 3"; browser back after a filter change leaves the page instead of undoing it.

Proposal

1. Search-params contract on the router adapter (#127 — already shipped)

interface StackRouter {
  Link, navigate, refresh, Image,          // existing (#127)
  getSearchParams(): URLSearchParams;
  setSearchParams(next: URLSearchParams, opts?: { replace?: boolean }): void;
}

Mapped per framework by the bindings (Next useSearchParams + router.replace; React Router useSearchParams; TanStack useSearch/navigate({ search })).

2. useListState helper in core

const [state, setState] = useListState("comments-moderation", {
  tab: { type: "string", default: "pending" },
  page: { type: "number", default: 1 },
});
// URL: /pages/comments/moderation?tab=spam&page=3

Scope (core only)

  • Extend the StackRouter contract + all three framework bindings with getSearchParams / setSearchParams
  • useListState in packages/stack/src/client/ + unit tests (types, defaults, replace/push, namespacing)
  • SSR: document/implement how loaders read initial list state from the request URL
  • Docs: router contract reference + useListState usage

Acceptance criteria

  • useListState round-trips state through the URL across all three framework bindings
  • Default values produce clean URLs; deviations serialize; back button undoes discrete changes
  • Unit-tested; validated on one real page (comments moderation) as part of the first Phase 2: per-plugin adoption sweep of the new core primitives #136 sweep that touches it

Plugin list-page migrations → #136.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions