skip to content

In Angular, when would you write a route's redirectTo as a function instead of a string, and what can that function use?

level: middleimportance: should knowfreq 38%

answer

  1. strings copy :params verbatim
  2. logic needs a function
  3. partial snapshot, not the full one
  4. runs in an injection context
  5. leading slash means absolute

basics

~20 s

Use a string when the target is fixed apart from copied :params; use a function when the target needs logic. The function receives a partial route snapshot (params, queryParams, data, url), can call inject(), and returns a string or UrlTree, synchronously or asynchronously.

solid answer

~40 s

A string `redirectTo` such as `'posts/:slug'` covers most cases: `:name` placeholders are filled from the matched route's parameters, and a leading slash makes it absolute. Since v18, `redirectTo` can also be a `RedirectFunction`. It is called during route matching with a partial snapshot — `params`, `queryParams`, `fragment`, `data`, `url`, `title`, and since v21.1 `paramMap`/`queryParamMap` — but not resolved data or the component, because matching is not finished. It runs in an injection context, so it can `inject()` a service, and it returns a string or a `UrlTree`; since v20 it may also return a Promise or Observable. I use it for things a string cannot express: mapping an old numeric id to a slug, choosing a target from a query parameter, or a locale-dependent default. A returned `UrlTree` is always treated as absolute.

code

ts · 18 lines
ts
import { inject } from '@angular/core';
import { Routes } from '@angular/router';
import { LegacySlugs } from './legacy-slugs';
import { PostDetail } from './posts/post-detail';

export const routes: Routes = [
  // String form: copy the parameter, absolute target
  { path: 'articles/:slug', redirectTo: '/posts/:slug' },
  // Function form: logic a template cannot express
  {
    path: 'blog/:id',
    redirectTo: (route) => {
      const slug = inject(LegacySlugs).forId(route.params['id']);
      return slug ? `/posts/${slug}` : '/not-found';
    },
  },
  { path: 'posts/:slug', component: PostDetail },
];

go deeper

for a junior

Recall that redirectTo can copy :params in a string and can be a function when the target needs logic.

for a middle

Explain relative versus absolute targets, the partial snapshot the function receives, inject() support and the allowed return types.

for a senior

Keep redirect functions fast and loop-free, keep permission logic in guards, and use them to retire legacy URLs safely.

for a principal

Own the URL migration plan: which legacy paths redirect, for how long, and how redirect behaviour is tested and monitored.

## Two forms of `redirectTo` A route with `redirectTo` does not render anything; when it matches, the router rewrites the URL and continues matching. **String form.** `redirectTo: 'posts/:slug'` is a URL template: - `:name` segments are replaced with the parameters the redirect route matched. A placeholder with no matching parameter is an error ("Cannot redirect to ... Cannot find ':name'"). - Without a leading slash the redirect is **relative**: it replaces the segments this route consumed at its level of the tree. - With a leading slash it is **absolute**: matching restarts from the root with the new URL. **Function form (since v18).** `redirectTo: (route) => ...` is a `RedirectFunction`: - It receives a **`PartialMatchRouteSnapshot`**: `routeConfig`, `url`, `params`, `queryParams`, `fragment`, `data`, `outlet`, `title`, and since v21.1 `paramMap` and `queryParamMap`. It is partial because the router is still matching; resolvers have not run and no component exists yet. - It runs in an **injection context**, so it can call `inject()` for a service. - It returns a **string** (interpreted like the string form) or a **`UrlTree`**. Since v20 the return may also be a **Promise** or **Observable** of either. - A returned `UrlTree` is always an absolute redirect. ## When to choose which | Need | String | Function | |---|---|---| | Rename a path, keep parameters | `'posts/:slug'` | overkill | | Default child route | `''` with `pathMatch: 'full'` | overkill | | Look up a slug from a legacy numeric id | not possible | inject a lookup service | | Pick a target from a query parameter or the time of day | not possible | read `queryParams`, compute | | Preserve or rewrite query parameters explicitly | limited | build a `UrlTree` with the query you want | ## Worked example: old blog URLs The blog used to publish `/blog/1234`; posts now live at `/posts/:slug`. A string redirect cannot convert an id into a slug, so a function does it: ```ts { path: 'blog/:id', redirectTo: (route) => { const slugs = inject(LegacySlugs); const slug = slugs.forId(route.params['id']); return slug ? `/posts/${slug}` : '/not-found'; }, } ``` If the lookup needs the network, the function can return a Promise; the navigation waits for it. ## Relative versus absolute inside child routes The difference only becomes visible below the top level. Inside the `children` of an `admin` route: - `{ path: '', redirectTo: 'users', pathMatch: 'full' }` is **relative**: it replaces the segments consumed at the child level, so `/admin` becomes `/admin/users`. - `{ path: '', redirectTo: '/users', pathMatch: 'full' }` is **absolute**: it throws away the parent segments too, so `/admin` becomes `/users`, and matching restarts from the root table. At the top level both forms produce the same URL, which is why the mistake usually surfaces only after a section is nested. ## Constraints worth knowing - A redirect route cannot also declare `component`, `loadComponent`, `children` or `loadChildren`; development-mode validation rejects those combinations. - A redirect route cannot have `canActivate` or `canMatch`: the validation message states that **redirects happen before guards are executed**. If access control should decide the destination, use a guard on the target route that returns a redirect instead. - Keep redirect functions **fast**. They run during matching for every navigation that reaches them, and an asynchronous one delays the whole navigation. - A function that returns a URL leading back to itself creates a redirect loop; the router's dev-mode loop detection targets absolute string redirects, so do not rely on it for functions. ## Redirect or guard? Redirects are about **URL shape**: old paths, defaults, aliases. Guards are about **permission** and run after matching. Putting authentication into a redirect function mixes the two and hides the rule from the route that needs protecting; the redirect cannot even coexist with guards on the same route. ## What interviewers listen for The string placeholder rules, relative versus absolute, what the function receives and why it is partial, `inject()` availability, the allowed return types with their version history, and the boundary with guards.

  • Why can't the redirect function read resolved data or the routed component?
    It runs during route matching, before the router has finished building the route tree, so resolvers have not run and no component is selected. That is why it receives a `PartialMatchRouteSnapshot` with only the fields known at that point: URL, params, query params, fragment, static data, title and the route config.
  • Can a redirect route also have canActivate to protect it?
    No. Development-mode validation rejects `redirectTo` combined with `canActivate` or `canMatch`, explaining that redirects happen before guards run. Put the guard on the destination route, where it can return a redirect of its own.

saying these in an interview costs you the question

  • redirectTo functions receive the full ActivatedRouteSnapshot with resolved data
  • inject() cannot be used inside a redirectTo function
  • A redirect route can also render a component
  • A redirect route can be protected with canActivate
  • redirectTo functions must return synchronously