URL-synced table state
Sync sort column, sort direction, active filters, current page, and rows-per-page to the URL
query string using history.replaceState. Table state survives a reload, links
are shareable, and the browser back button restores exactly where the user was.
Try sorting a column, typing in a filter, or changing the page — watch the URL update live. Copy the URL and open it in a new tab.
Reading initial state from the URL
Parse URLSearchParams once on mount and use the result to seed all state.
Wrap it in useMemo so it only runs once even in strict mode:
function readParams() {
const p = new URLSearchParams(window.location.search);
return {
sortId: p.get('sort') ?? '',
sortDir: p.get('dir') === 'desc' ? SortOrder.DESC : SortOrder.ASC,
page: parseInt(p.get('page') ?? '1', 10),
perPage: parseInt(p.get('per') ?? '5', 10),
// filters stored as f_<columnId>=<value>
filters: Object.fromEntries(
[...p.entries()]
.filter(([k]) => k.startsWith('f_'))
.map(([k, v]) => [k.slice(2), v]),
),
};
}
const init = useMemo(() => readParams(), []);
const [sortId, setSortId] = useState(init.sortId);
const [sortDir, setSortDir] = useState(init.sortDir);
const [page, setPage] = useState(init.page);
const [filters, setFilters] = useState(/* map init.filters to FilterState */); Writing state back to the URL
A useEffect that depends on all state writes to the URL on every change.
Use replaceState (not pushState) so navigating within the table
doesn't pollute the browser history:
useEffect(() => {
const p = new URLSearchParams();
if (sortId) { p.set('sort', sortId); p.set('dir', sortDir === SortOrder.DESC ? 'desc' : 'asc'); }
if (page > 1) p.set('page', String(page));
if (perPage !== DEFAULT_PER_PAGE) p.set('per', String(perPage));
Object.entries(filterStrings).forEach(([k, v]) => { if (v) p.set(`f_${k}`, v); });
const qs = p.toString();
history.replaceState(null, '', qs ? `?${qs}` : window.location.pathname);
}, [sortId, sortDir, page, perPage, filterStrings]); Wiring to DataTable
Pass the state as controlled props. Use sortServer to prevent the table from
re-sorting internally — the URL is the source of truth, so the component controls sort:
<DataTable
columns={columns}
data={data}
sortServer
defaultSortFieldId={sortId || undefined}
defaultSortAsc={sortDir === SortOrder.ASC}
onSort={(col, dir) => { setSortId(col.id as string); setSortDir(dir); setPage(1); }}
filterValues={filters}
onFilterChange={(columnId, next) => { setFilters(prev => ({ ...prev, [columnId]: next })); setPage(1); }}
pagination
paginationDefaultPage={page}
paginationPerPage={perPage}
onChangePage={p => setPage(p)}
onChangeRowsPerPage={(pp, p) => { setPerPage(pp); setPage(p); }}
/> Reset page to 1 whenever sort or filters change so you don't land on a now-empty page.