Creating reusable Router Signals APIs in Angular 🗺️
Building single page applications with Angular often involves working with the Angular Router.
When working with the router there are a few common patterns that we often find ourselves repeating.
For example, we often need to grab the current route params or query params and use them in our components.
In the above example, we are using the ActivatedRoute to grab the current route params and then using the ProductsService to fetch the product details.
The issue with the above code is that when the route params change, the component will not update the productName value. Let's fix that.
Let's make this more declarative and reactive using observables.
Using Observables to react to route changes
Just like we used the snapshot to grab the route params, we can use the params observable to listen to route params changes.
Here we have a declarative and reactive way to fetch the product details when the route params change using observables, that will also cancel the previous request when the route params change (this is useful when the user is navigating quickly between routes).
We are using the async pipe to subscribe to the productName$ observable and display the product name in the template.
Refactoring to Signals
We can take this a step further and refactor the observables into signals. Let's use the toSignal function to convert the observables into signals.
Now we have a productName signal that will be updated when the productName$ observable emits a new value. This is great! We can now use the productName signal in our template without the need for the async pipe.
Can we do better? To, write less code maybe?
Creating a reusable router signals API
We can create a reusable router signals API that will allow us to easily grab the current route params and query params and use them in a declarative way using signals.
We want to be able to inject params that should return us all the route params as a signal, and if we pass a key, it should return us the value of that key as a signal.
Something like this:
More info about computedAsync you can find in the docs.
I wrote more about it here: Building ComputedAsync for Signals in Angular
Implementing the injectParams function
Because our function will inject the ActivatedRoute we need to make sure we are in an injection context. We can use the assertInInjectionContext function to do that.
Now that we have access to the ActivatedRoute we can grab the route params and convert them into a signal using the toSignal function.
Params emit synchronously, so to enforce it, let's use toSignal with requireSync option.
We can also add support for passing a key to the injectParams function. If a key is passed, we should return the value of that key as a signal.
We can extend the injectParams function to also support a transform function that will allow us to transform the params into a different value.
It will be used like this:
While we add the support for the transform function, we can also make the function generic to support the return type of the transform function.
As we can see, we have created a reusable router signals API that will allow us to easily grab the current route params and use them in a declarative way using signals.
We can do the same for the query params using the queryParams observable from the ActivatedRoute.
But, you don't need to do that, because it's already done for you in the ngxtension library.
injectQueryParams and injectParams
These two functions are already published by the ngxtension library
They are fully tested and are already being used in multiple projects.
Using withComponentInputBinding() with signal inputs
In v17.1 Angular introduced signal inputs, and before that introduced the withComponentInputBinding() function which enables us to have inputs with the same name as our params or query params.
I wrote about that function here: Bind Route Info to Component Inputs.
What's the difference between these functions and signal inputs? Two things:
Being explicit about where your data comes from
If you have an input called productId, do you expect it to come from a parent component or the route params?
What if you need all the params (or query params)
We cannot grab all the params and queryParams using inputs, so we still have to rely on injecting ActivatedRoute and listen to observable changes. And that's something injectParams solves by default.
That's all folks! Hope you liked it!
Thanks for reading!
If this article was interesting and useful to you, and you want to learn more about Angular, support me by buying me a coffee ☕️ or follow me on X (formerly Twitter) @Enea_Jahollari where I tweet and blog a lot about Angular latest news, signals, videos, podcasts, updates, RFCs, pull requests and so much more. 💎



