URL-Synced List with Routeit
Problem
When a user filters or paginates a list and navigates away, pressing the browser Back button resets the list to its initial state. List state should live in the URL so forward/backward navigation preserves exact pagination, search, and filter state.
Solution
Subscribe to source state changes and call the router's navigate with replace: true to keep the URL in sync. On load, hydrate the source from the current route's query params.
ts
import { createRouter } from '@vielzeug/routeit';
import { createRemoteSource, encodeRemoteQueryParams } from '@vielzeug/sourceit';
const router = createRouter({
routes: {
issues: { path: '/issues' },
},
});
const source = createRemoteSource({
fetch: ({ limit, page, search, sort }) =>
fetch(`/api/issues?${new URLSearchParams({ limit: String(limit), page: String(page), search: search ?? '' })}`)
.then((r) => r.json()),
sort: { by: 'updatedAt', dir: 'desc' },
limit: 20,
});
// Hydrate source from current URL on page load
const route = router.state;
await source.fromQueryParams(route.location.query);
await source.ready();
// Keep URL in sync after every user interaction
const syncUrl = () => {
void router.navigate(
{ name: 'issues', query: encodeRemoteQueryParams(source.toQuery()) },
{ replace: true },
);
};
const stop = source.subscribe(syncUrl);
// Example interaction
source.search('regression');
await source.commit();
await source.ready();
// URL is now /issues?search=regression&page=1&...Pitfalls
source.subscribe(syncUrl)fires on every state change including internal transitions likeisLoadingtoggling. Only callnavigatewhen the serialized query string has actually changed to avoid redundant history entries.- Calling
router.navigate()inside the subscription can create a feedback loop if the router emits a new route event that the subscription reacts to. Use asyncingflag to break the cycle. fromQueryParams()usesdecodeRemoteQueryParams()under the hood and expects a flat string-keyed query object.