History
import { historyPush, historyReplace, objectToURLSearchParams } from '@studiometa/js-toolkit/utils';Writing the URL
import { historyPush, historyReplace } from '@studiometa/js-toolkit/utils';
historyPush({ path: '/products', search: { page: 2, sort: 'price' } });
historyReplace({ search: { page: 3 } });
historyPush({ hash: 'section-2' });interface HistoryOptions {
path?: string;
search?: URLSearchParams | Record<string, SearchParamInput>;
hash?: string;
}Each part is optional, and an omitted part is kept. historyReplace({ search }) keeps the path and the hash; historyPush({ hash }) keeps the path and the query string.
That is the whole reason these exist over history.pushState(): rebuilding a URL from location by hand is where a filter update loses the hash, or a hash update drops the query string.
historyPush
historyPush(options: HistoryOptions, data?: unknown, title?: string): voidAdds a history entry. This is what makes a state navigable with the back button.
historyReplace
historyReplace(options: HistoryOptions, data?: unknown, title?: string): voidReplaces the current entry. This is what keeps a scroll position or a tab index out of the history.
Building the query string
objectToURLSearchParams
objectToURLSearchParams(
object: Record<string, SearchParamInput>,
defaultSearch?: string,
): URLSearchParamsimport { objectToURLSearchParams } from '@studiometa/js-toolkit/utils';
objectToURLSearchParams({ page: 2, tags: ['a', 'b'], open: true });
objectToURLSearchParams({ page: 2 }, location.search);SearchParamInput accepts a string, a number, a boolean, null, undefined, an array of those, or a nested object of those — so a filter state serializes without being flattened first.
defaultSearch is the starting point, so the result merges into an existing query string instead of replacing it.
See also
For a URL that is also a store you can read, write and observe, use createUrlSearchParamsStorage() — it is these functions behind a typed key-value surface, with popstate wired up.