Skip to main content

Pagination & URL State (nuqs)

Overview

Our data tables use URL-synchronized state via nuqs. Pagination should be stable: selecting rows or toggling non-paging controls must not reset the current page to 1. This guide explains:
  • How pagination parameters are modeled
  • Which URL params trigger a page reset
  • The OMIT_FIELDS list used to prevent unwanted resets
  • Practical recommendations when adding new URL params

Key URL Parameters

  • page: current page (1-based)
  • size: page size
  • sort: sort field key
  • asc: sort direction (boolean)
  • filter: whether filtering UI is enabled (boolean)
  • Additional dynamic filter params (e.g., doc_date, status, etc.)
  • Selection and UI-only params (e.g., selected)

Page Reset Rules

Changing actual filter values should reset to page 1 so users see results from the beginning. However, changes to purely presentational or ephemeral state (like selection) must not trigger a reset. We implement this using an omit list during change detection.

OMIT_FIELDS

Source of truth in the webapp utility:
const OMIT_FIELDS = [
  "page",
  "pageSize",
  "asc",
  "sort",
  "filter",
  "tab",
  // Do not reset page when selection changes
  "selected",
] as const;
Any differences between the previous and current processed params after omitting these fields are considered a “meaningful change” and will reset page to the default.

Where Reset Happens

The reset to page 1 is applied in processSearchParams when a meaningful change is detected:
const filteredProcessedParams = omit({ ...processedParams }, OMIT_FIELDS);

if (previousParsedParams && !isEqual(previousParsedParams, filteredProcessedParams)) {
  processedParams.page = DEFAULT_PAGE.toString();
}

previousParsedParams = filteredProcessedParams;
In addition, the pagination/UI hook useQueryFiltersSync resets page when any filter group (not omitted) changes, using a debounced effect to avoid jitter during typing:
if (JSON.stringify(prevFilters.current) !== JSON.stringify(filters)) {
  if (debounceTimeoutRef.current) clearTimeout(debounceTimeoutRef.current);
  debounceTimeoutRef.current = setTimeout(() => {
    setPage(DEFAULT_PAGE.toString());
    prevFilters.current = filters;
  }, DEBOUNCE_INTERVAL);
}

What Should Not Reset Pagination

The following changes must not reset the page:
  • Row selection changes: selected
  • Sorting toggles: sort, asc
  • Pagination controls themselves: page, size
  • View toggles/tabs: tab
  • UI-only flags that don’t change the dataset shape
Ensure these live in OMIT_FIELDS or otherwise do not affect the reset logic.

What Should Reset Pagination

Any change that alters the dataset shape/order enough that page N might no longer contain the same items, for example:
  • Adding/removing/changing filter values (e.g., doc_date, status)
  • Toggling filter that enables filter mode and reveals active filters
  • Changing object scoping parameters
These should not be in OMIT_FIELDS so that the system resets to page 1.

Best Practices When Adding Params

  1. Classify the parameter:
    • Data-affecting (should reset) vs. UI-only/ephemeral (should not reset)
  2. If it should not reset, add its key to OMIT_FIELDS in search-params-utils.ts.
  3. Keep pagination stable on selection and sort changes.
  4. Debounce page resets on filter edits to avoid flicker.
  5. Keep URL key mapping consistent (see search-params-config for pageSizesize).

Example: Row Selection Should Not Reset

In AR Reconciliation, selecting a row updates the selected query param. Because selected is listed in OMIT_FIELDS, changing selection will not reset page.

Troubleshooting

  • Pagination resets unexpectedly: Verify the param is in OMIT_FIELDS if it is UI-only.
  • Page doesn’t reset when filters change: Ensure those filter keys are NOT in OMIT_FIELDS and that they flow through processSearchParams.
  • Jumping during typing: Confirm debounce is applied in useQueryFiltersSync.

By following these rules and maintaining OMIT_FIELDS, pagination remains predictable while filters and controls work as expected.
I