Angular Router: Understanding Router State
Angular Router: Understanding Router State
Victor Savkin is a co-founder of nrwl.io, providing Angular consulting to enterprise teams. He was previously on the Angular core team at Google, and built the dependency injection, change detection, forms, and router modules.
An Angular application is a tree of components. Some of these components are reusable UI components (e.g., list, table), and some are application components, which represent screens or some logical parts of the application. The router cares about application components, or, to be more specific, about their arrangements. Let’s call such component arrangements router states. In other words, a router state is an arrangement of application components that defines what is visible on the screen.
In this article we will look at RouterState in depth.
This article is based on the Angular Router book, which you can find herehttps://leanpub.com/router. The book goes beyond a how-to-get-started guide and talks about the router in depth. The mental model, design constraints, and the subtleties of the API-everything is covered. If you enjoy the article, check out the book!
During a navigation, after redirects have been applied, the router creates a RouterStateSnapshot. What is RouterStateSnapshot, and how is it different from RouterState?
RouteStateSnapshot is an immutable data structure representing the state of the router at a particular moment in time. Any time a component is added or removed or parameter is updated, a new snapshot is created.
RouterState is similar to RouteStateSnapshot, except that it represents the state of the router changing over time.
As you can see RouterStateSnapshot is a tree of activated route snapshots. Every node in this tree knows about the “consumed” URL segments, the extracted parameters, and the resolved data. To make it clearer, let’s look at this example:
When we are navigating to “/inbox/33/messages/44”, the router will look at the URL and will construct the following RouterStateSnapshot:
After that the router will instantiate ConversationCmp with MessageCmp in it.
Now imagine we are navigating to a different URL: “/inbox/33/messages/45”, which will result in the following snapshot:
To avoid unnecessary DOM modifications, the router will reuse the components when the parameters of the corresponding routes change. In this example, the id parameter of the message component has changed from 44 to 45. This means that we cannot just inject an ActivatedRouteSnapshot into MessageCmp because the snapshot will always have the id parameter set to 44, i.e., it will get stale.
The router state snapshot represents the state of the application at a moment in time, hence the name ‘snapshot’. But components can stay active for hours, and the data they show can change. So having only snapshots won’t cut it — we need a data structure that allows us to deal with changes.
RouterState and ActivatedRoute are similar to their snapshot counterparts except that they expose all the values as observables, which are great for dealing with values changing over time.
Any component instantiated by the router can inject its ActivatedRoute.
If we navigate from “/inbox/33/messages/44” to “/inbox/33/messages/45”, the data observable will emit a new set of data with the new message object, and the component will display Message 45.
The router exposes parameters and data as observables, which is convenient most of the time, but not always. Sometimes what we want is a snapshot of the state that we can examine at once.
ActivatedRoute provides access to the url, params, data, queryParams, and fragment observables. We will look at each of them in detail, but first let’s examine the relationships between them.
URL changes are the source of any changes in a route. And it has to be this way as the user has the ability to modify the location directly.
Any time the URL changes, the router derives a new set of parameters from it: the router takes the positional parameters (e.g., ‘:id’) of the matched URL segments and the matrix parameters of the last matched URL segment and combines those. This operation is pure: the URL has to change for the parameters to change. Or in other words, the same URL will always result in the same set of parameters.
Next, the router invokes the route’s data resolvers and combines the result with the provided static data. Since data resolvers are arbitrary functions, the router cannot guarantee that you will get the same object when given the same URL. Even more, often this cannot be the case! The URL contains the id of a resource, which is fixed, and data resolvers fetch the content of that resource, which often varies over time.
Finally, the activated route provides the queryParams and fragment observables. In opposite to other observables, that are scoped to a particular route, query parameters and fragment are shared across multiple routes.
Given the following:
And navigating first to “/inbox/33/messages/44” and then to “/inbox/33/messages/45”, we will see:
url [{path: ‘messages’, params: {}}, {path: ‘44’, params: {}}]url [{path: ‘messages’, params: {}}, {path: ‘45’, params: {}}]
We do not often listen to URL changes as those are too low level. One use case where it can be practical is when a component is activated by a wildcard route. Since in this case the array of URL segments is not fixed, it might be useful to examine it to show different data to the user.
Given the following:
And when navigating first to “/inbox/33/messages;a=1/44;b=1” and then to “/inbox/33/messages;a=2/45;b=2”, we will see
params {id: ‘44’, b: ‘1’}params {id: ‘45’, b: ‘2’}
First thing to note is that the id parameter is a string (when dealing with URLs we always work with strings). Second, the route gets only the matrix parameters of its last URL segment. That is why the ‘a’ parameter is not present.
Let’s tweak the configuration from above to see how the data observable works.
Where MessageResolver is defined as follows:
The data property is used for passing a fixed object to an activated route. It does not change throughout the lifetime of the application. The resolve property is used for dynamic data.
Note that in the configuration above the line “message: MessageResolver” does not tell the router to instantiate the resolver. It instructs the router to fetch one using dependency injection. This means that you have to register “MessageResolver” in the list of providers somewhere.
Once the router has fetched the resolver, it will call the ‘resolve’ method on it. The method can return a promise, an observable, or any other object. If the return value is a promise or an observable, the router will wait for that promise or observable to complete before proceeding with the activation.
The resolver does not have to be a class implementing the `Resolve` interface. It can also be a function:
The router combines the resolved and static data into a single property, which you can access, as follows:
When navigating first to “/inbox/33/message/44” and then to “/inbox/33/messages/45”, we will see
data {allowReplyAll: true, message: {id: 44, title: ‘Rx Rocks’, …}}data {allowReplyAll: true, message: {id: 45, title: ‘Angular Rocks’, …}}
In opposite to other observables, that are scoped to a particular route, query parameters and fragment are shared across multiple routes.
If you liked this, click the? below so other people will see this here on Medium. Follow @victorsavkin to read more about Angular.