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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions