Which @angular/* package(s) are relevant/related to the feature request?
router
Description
Context: I built a small utility at work to two-way bind a signal value to a query param in the url. This is useful to store filters on a data table as url data and restore it when navigating or to share a link. It also apply to local config for pages that we want to embed in the url for sharing purposes. It's currently in use. We are fully in the big signal era of angular, this is generally applicable and so much useful that I want to share it. Also this is not trivial and I got it working well so I hope my design would inspire you making a native version.
My research on the subject showed that this subject is quite poorly addressed. The closest thing I found is this issue where the comment I link says it is (was ?) on the roadmap and in the top 5 prioritized project (#59877 (comment)). So I'd like to put one design constraint on the record before the API shape settles.
The problem: independent per-param writes race on the URL.
Most takes on query-param signals bind one signal to one param, each owning its own URL write. That's ergonomic with a single param, but breaks down as soon as several params change in the same tick — e.g. a "clear filters" button resetting search, page and category at once:
clear() {
this.search.set("");
this.page.set(1);
this.category.set(null);
}
If each signal writes the URL independently, that's three writes in one change-detection pass. With queryParamsHandling: 'merge', each write reads the current URL, merges its one key, and writes back. Two consequences:
- Churn — the URL (and Location/history) is rewritten N times for one logical change.
- Lost updates — depending on timing, a later write can read a URL that doesn't yet reflect an
earlier one, and clobber it: you end up with a URL missing a param you did set.
Debouncing hides the churn but adds latency and is still not atomic. So here is my proposed solution
Proposed solution
First I suggest a very simple API : queryParamSignal<T>(key: string, initalValue: T, options?: ...): WritableSignal<T> (link to the implementation below)
This gives you back a writable signal which value is synchronized to a query param of a given key. It is two ways meaning that a navigation changing a param would update the signal. options is a complex type that depends on the type T. It is an object with a flag for array values, and a parse and stringify function to parse/serialize value from and to a query param value. The API and typing is built to be as minimal as possible to use while giving great typing.
In use it look like this :
@Component({ ... })
export class MyComponent {
// string -> options optional
search = queryParamSignal("q", "");
// number -> parse required (compile error if you forget it)
page = queryParamSignal("page", 1, { parse: (v) => Number(v) || 1 });
// string[] -> array:true; empty string dropped from the URL
tags = queryParamSignal("tags", [] as string[], {
array: true,
stringify: (v) => (v.length ? v : null),
});
}
To resolve the atomic multi-param case I use a shared service. Params signals don't write the URL themselves, they register into the service, and a single effect reads every registered signal, assembles one queryParams object, and performs one location.replaceState. Since signals are glitch-free, N updates in one tick collapse into one atomic URL write. I also used the location.replaceState to avoid triggering navigation and write in signals back.
To avoid bloating this issue I link the code to my full implementation of this API : https://gist.github.com/glimps-fde/a0a3ad3831133aed277f537c5bb24ad6
This currently fits my internal use case; just works, and it is a very powerful pattern for us. If the Angular team sees value in the approach but wants it to behave differently (navigation semantics, collision policy, API shape, edge cases I haven't hit), I'm happy to iterate on the prototype based on your feedback, or to bring any of this into the upcoming router-signals design discussion when it opens.
Alternatives considered
- Per-signal URL writes : ergonomic, but subject to the churn/lost-update race described above when several params change in the same tick.
- Debouncing the writes: hides the churn but adds latency and is still not atomic — the race becomes less likely, not impossible.
toSignal(route.queryParams) + manual router.navigate calls: read-only side is fine, but the write side pushes the batching problem onto every call site.
Which @angular/* package(s) are relevant/related to the feature request?
router
Description
Context: I built a small utility at work to two-way bind a signal value to a query param in the url. This is useful to store filters on a data table as url data and restore it when navigating or to share a link. It also apply to local config for pages that we want to embed in the url for sharing purposes. It's currently in use. We are fully in the big signal era of angular, this is generally applicable and so much useful that I want to share it. Also this is not trivial and I got it working well so I hope my design would inspire you making a native version.
My research on the subject showed that this subject is quite poorly addressed. The closest thing I found is this issue where the comment I link says it is (was ?) on the roadmap and in the top 5 prioritized project (#59877 (comment)). So I'd like to put one design constraint on the record before the API shape settles.
The problem: independent per-param writes race on the URL.
Most takes on query-param signals bind one signal to one param, each owning its own URL write. That's ergonomic with a single param, but breaks down as soon as several params change in the same tick — e.g. a "clear filters" button resetting search, page and category at once:
If each signal writes the URL independently, that's three writes in one change-detection pass. With
queryParamsHandling: 'merge', each write reads the current URL, merges its one key, and writes back. Two consequences:earlier one, and clobber it: you end up with a URL missing a param you did set.
Debouncing hides the churn but adds latency and is still not atomic. So here is my proposed solution
Proposed solution
First I suggest a very simple API :
queryParamSignal<T>(key: string, initalValue: T, options?: ...): WritableSignal<T>(link to the implementation below)This gives you back a writable signal which value is synchronized to a query param of a given key. It is two ways meaning that a navigation changing a param would update the signal.
optionsis a complex type that depends on the type T. It is an object with a flag for array values, and a parse and stringify function to parse/serialize value from and to a query param value. The API and typing is built to be as minimal as possible to use while giving great typing.In use it look like this :
To resolve the atomic multi-param case I use a shared service. Params signals don't write the URL themselves, they register into the service, and a single
effectreads every registered signal, assembles onequeryParamsobject, and performs onelocation.replaceState. Since signals are glitch-free, N updates in one tick collapse into one atomic URL write. I also used thelocation.replaceStateto avoid triggering navigation and write in signals back.To avoid bloating this issue I link the code to my full implementation of this API : https://gist.github.com/glimps-fde/a0a3ad3831133aed277f537c5bb24ad6
This currently fits my internal use case; just works, and it is a very powerful pattern for us. If the Angular team sees value in the approach but wants it to behave differently (navigation semantics, collision policy, API shape, edge cases I haven't hit), I'm happy to iterate on the prototype based on your feedback, or to bring any of this into the upcoming router-signals design discussion when it opens.
Alternatives considered
toSignal(route.queryParams)+ manualrouter.navigatecalls: read-only side is fine, but the write side pushes the batching problem onto every call site.