At your last stop you mapped out the Tokaido roads, and among them sat this entry:
{ path: 'samurai/:id', component: SamuraiDetailComponent }. The colon in front of id told the router that this stretch of the address is variable. Opening /samurai/7 brings up the profile screen, opening /samurai/128 brings up exactly the same screen - and that is where the trouble starts. The component that appears has no idea which warrior it is supposed to be describing. The road carried it to the right door and then fell silent.In the Academy it looks like this, @name: a runner arrives from an outpost and hands you a sealed scroll. The seal carries a warrior's number, and in the margin someone has jotted down the small stuff - how to sort the roster, which page to start from. The road delivered the scroll to the gate, but nobody is going to read it on your behalf. Your job is to break the seal. You will learn two things here: how to read the address you are standing on, and how to issue the order to march to a different address straight from the code of your class.
Angular keeps the description of the route that is currently open inside a service named
ActivatedRoute. A service is a ready-made object that the framework builds by itself and hands you on request - you do not create it with new, you ask for it. The asking is done with the inject() function from the @angular/core package: you name the service, you get back a working instance. That gesture is called dependency injection, and you will take it apart piece by piece in the module on the craftsmen's guilds. Here, one sentence is enough: inject(ActivatedRoute) returns the scroll describing the route you are standing on right now.Learn straight away what is not worth hunting for. The names
RouteParams and NavigationService sound perfectly sensible, but in Angular they simply do not exist - the @angular/router package exports nothing of the sort, and the compiler will reject such an import outright. There is a Router service, which you will meet in the second half of the lesson, except that it exists to carry you between routes, not to read from the one you are on. For reading parameters there is exactly one address: ActivatedRoute.1import { Component, inject } from '@angular/core';
2import { ActivatedRoute } from '@angular/router';
3
4@Component({
5 selector: 'app-samurai-detail',
6 standalone: true,
7 template: '<h1>Samurai profile</h1>'
8})
9export class SamuraiDetailComponent {
10 private route = inject(ActivatedRoute);
11}On screen nothing changed - the template still shows the same heading, with no number anywhere in it. The only thing that changed is that the
route field now holds the scroll, and you can look inside it. Notice two details. First, there is no constructor here at all: you call inject() right at the field declaration, and by that moment Angular already has the service prepared. Second, the word private says that the scroll is the class's internal business - the template does not reach for it, and it should not.The simplest way to read the scroll is to look at it once, at the gate.
ActivatedRoute has a field called snapshot, which is exactly what the name says: the frozen state of the route at the moment you ask for it. Inside the snapshot sits paramMap, the map of path parameters - an object of type ParamMap that offers a get(name) method. You pass it the parameter name without the colon, so 'id', and you get back a value, or null when the address holds no such parameter.The value always comes back as text, even for
/samurai/7 - you get the string '7', not a number. And since get() may return null, that case has to be planned for. The ?? operator does the job; it is called the nullish coalescing operator, and it hands you the right-hand side only when the left-hand side is null or undefined. The whole read is a chain of four links on a single line: this.route, then .snapshot, then .paramMap, and finally .get('id'). We will put it inside ngOnInit, the same lifecycle hook you met during the warrior's rituals, which means the class also declares implements OnInit.1import { Component, inject, OnInit } from '@angular/core';
2import { ActivatedRoute } from '@angular/router';
3
4@Component({
5 selector: 'app-samurai-detail',
6 standalone: true,
7 template: '<h1>Samurai #{{ samuraiId }}</h1>'
8})
9export class SamuraiDetailComponent implements OnInit {
10 private route = inject(ActivatedRoute);
11 samuraiId = '';
12
13 ngOnInit(): void {
14 this.samuraiId = this.route.snapshot.paramMap.get('id') ?? '';
15 }
16}Opening
/samurai/7 now shows "Samurai #7". What did not change: the routes file. The samurai/:id entry looks exactly as it did yesterday, and the router still pushes nothing into the component by itself - it is the component that reaches for the data. Watch out for one trap: the name :id in the route config and the string 'id' in get('id') must match letter for letter. A typo raises no error at all; it quietly returns null, ?? substitutes the empty string, and you end up staring at a blank heading and blaming the router.A path parameter is mandatory: the address
/samurai with no number does not match the samurai/:id route at all, because a whole segment is missing, so the router looks for a different route or shows an error screen. But scrolls have a margin too, where the runner scribbles the optional things - the sort order, the page number, the chosen filter. In an address those notes live after the question mark, as in /samurai?sort=name, and they are called query parameters. Angular keeps them in a separate map named queryParamMap, served by the very same get() method and following the very same rule that a missing parameter yields null.There is one difference, and it matters: query parameters take no part in route matching. The addresses
/samurai/7 and /samurai/7?sort=level land on the same route and the same component. That makes them the right place for everything the screen can open correctly without. Below I add one field and one line of reading to the class you already know.1// the same class, imports unchanged
2sortOrder = 'default';
3
4ngOnInit(): void {
5 this.samuraiId = this.route.snapshot.paramMap.get('id') ?? '';
6 this.sortOrder = this.route.snapshot.queryParamMap.get('sort') ?? 'default';
7}Opening
/samurai/7?sort=level now gives you samuraiId equal to '7' and sortOrder equal to 'level', while plain /samurai/7 leaves sortOrder holding the fallback 'default'. Notice that neither the way of reading nor the handling of a missing value changed - the only thing that changed is which map you reach into: paramMap for path segments, queryParamMap for the notes after the question mark. That is the single distinction you have to keep straight here.Now the most important moment of the lesson. You are standing on
/samurai/7 and you click a "next warrior" link pointing at /samurai/8. The address in the bar changes, the router matches the same route and the same component, so it does not build it again - it reuses the existing instance. And since the component is not created anew, ngOnInit is never called a second time. Your samuraiId field keeps the seven while the address bar clearly shows an eight.It is worth understanding exactly where the fault lies, because it is not what intuition suggests. The snapshot itself is refreshed on every navigation - if you read
this.route.snapshot.paramMap.get('id') after the transition, you would get the eight. The problem is that nobody reads it any more, because there is no occasion to. What you need is something that notifies you of the change by itself.That is what
paramMap read without snapshot, straight off ActivatedRoute, is for. It is not a single value but a stream of values arriving over time, in other words an Observable. To get anything out of it you sign up for notifications with the subscribe() method, handing it a function that Angular will call for every new value. In Academy terms: snapshot is a scroll collected once at the gate, while subscribe() is a standing runner who comes sprinting with a fresh scroll every time the order changes.1ngOnInit(): void {
2 this.route.paramMap.subscribe(params => {
3 this.samuraiId = params.get('id') ?? '';
4 });
5}The first line opens the subscription, the second performs the read, the third closes the callback and the call - and that is the order in which this code sits in the file. The
params object is of exactly the same type as snapshot.paramMap, so the get() method and the null handling through ?? look identical. Angular calls that function immediately when you enter the route, and then once more on every move to another number. Nothing changed except the moment of reading: it went from one-off to repeatable.When should you reach for which? Here is a simple rule, @name: if the view contains any link at all leading to the same route with a different parameter, take the stream. In every other case the snapshot is shorter, easier to read, and leaves no subscription behind. A subscription you do open ought to be cleaned up when the component is destroyed, that is in
ngOnDestroy - the tools that do this elegantly are waiting for you with the ninja scouts, in the module on RxJS. There is also a third road: the withComponentInputBinding() option passed when configuring the router makes route parameters flow directly into the component's inputs. It needs knowledge of signals that you will pick up later, so today we stay with ActivatedRoute.Up to now
routerLink in the template did the moving for you. Very often, though, the decision to go somewhere is taken in code: after a form is saved, after a successful login, after a warrior is picked off a list. That is when you need the second service from @angular/router, the one called Router. The division of duties is sharp and worth memorising once and for all: ActivatedRoute describes where you stand, Router carries you elsewhere.Two other names get confused here.
RouterOutlet is not a service but a directive - the <router-outlet> tag that marks, inside a template, the spot where the route's component gets rendered; you cannot inject it in order to go anywhere. Location from the @angular/common package, in turn, is a thin wrapper over the browser's history, with methods such as back() and go(). It can send the user back or swap the address, but it knows nothing about your route configuration and does not trigger matching. For going to a route defined in the routes file there is Router, and only Router.1import { Component, inject } from '@angular/core';
2import { Router } from '@angular/router';
3
4@Component({
5 selector: 'app-samurai-list',
6 standalone: true,
7 template: '<button (click)="goToSamurai(7)">Show warrior</button>'
8})
9export class SamuraiListComponent {
10 private router = inject(Router);
11
12 goToSamurai(id: number): void {
13 this.router.navigate(['/samurai', id]);
14 }
15}Clicking the button moves the user to
/samurai/7, and there the component from the first half of the lesson speaks up and reads the seven. The most important detail sits inside the brackets: the navigate() method takes an array of segments, not a finished string. Writing ['/samurai', id] means "the samurai route, and this number inside it", and it is Angular that glues the pieces together with a slash and encodes any special characters in them. A number in the array is perfectly allowed - the router turns it into text, though on the reading side it will still come back as a string. The whole line reads left to right: this.router, then .navigate(, then ['/samurai', id], and ); at the end.Since an address can carry query parameters, there has to be a way of adding them during navigation. The
navigate() method takes a second, optional argument: an object of extra settings for the journey. The queryParams field in that object is an ordinary set of keys and values that Angular will glue onto the address after the question mark.1sortByName(): void {
2 this.router.navigate(['/samurai'], {
3 queryParams: { sort: 'name', order: 'asc' }
4 });
5}After this call the address bar shows
/samurai?sort=name&order=asc. Route matching did not change along the way - the router still picks the samurai route, exactly the same one it would pick without the notes, because query parameters take no part in matching. The only thing that changed is the address that a component reading queryParamMap will see.By default every new navigation throws away the query parameters that were there before. If the user has set a sort order and some filters and you move them to the next screen, the notes are lost. You control this with the
queryParamsHandling field, which takes three sensible values: 'preserve' keeps the existing notes and ignores the ones you have just passed, 'merge' combines the old with the new, and 'replace' restores the default behaviour. I recommend 'merge' as your first choice, @name, because 'preserve' quietly bins the parameters handed to it in the very same call, and that can cost you an hour of hunting.1goToSamuraiKeepingFilters(id: number): void {
2 this.router.navigate(['/samurai', id], {
3 queryParamsHandling: 'preserve'
4 });
5}If the user was on
/samurai?sort=level, then after this call they land on /samurai/7?sort=level - the warrior number changed, the note about sorting survived. What did not change is the method itself: it is still navigate() with an array of segments, with just one option added in the second argument.Sometimes the address reaches you in one piece and there is nothing to build an array from. The classic case is an address remembered before the login screen, so that the user can be dropped back where they started once they sign in. Carving such a string into segments would be a waste of effort, so
Router offers a second method: navigateByUrl(). It takes the entire address as a single string, query parameters and the fragment after the hash included.1goToSavedUrl(): void {
2 this.router.navigateByUrl('/samurai/123?sort=level');
3}Do not try the same thing with
navigate() - the call navigate('/samurai/123') will not compile, because the first argument of that method has to be an array and TypeScript rejects a lone string. Do not go looking for goTo() or redirect() either: the Router service has neither, and the compiler will tell you plainly that no such property exists. The confusing one is redirectTo, which you do know from route configuration, but that is a field on an entry in the routes array, not a method on the service.Day to day I recommend
navigate() as your default: it joins segments safely, encodes special characters, and accepts options such as queryParams. Reach for navigateByUrl() when the address arrives from outside as finished text and taking it apart would be craft for craft's sake.Remember this, @name:
ActivatedRoute is the scroll you read and Router is the order to march that you issue - one glance at the gate is enough while you stand still, but when the road shifts under your feet, you need a runner.