Question
In Angular, how can you navigate from one routed component to another while passing data along, without rendering the target component as a child directive?
For example, suppose FirstComponent contains a button that should navigate to SecondComponent and make some values available there.
<div class="button" (click)="routeWithData()">Pass data and route</div>
export class FirstComponent {
constructor(private router: Router) {}
property1!: number;
property2!: string;
property3!: TypeXY; // custom object, not a primitive
routeWithData() {
// navigate here
}
}
A simple navigation might look like this:
this.router.navigate(['SecondComponent']);
And passing simple values in the URL might look like this:
this.router.navigate(['SecondComponent', { p1: this.property1, p2: this.property2 }]);
The concern is that URL-based routing works well for primitives, but not for complex objects like property3.
Another option would be to render the target component directly and pass data with @Input():
<app-second [p3]="property3"></app-second>
But that is not the goal here, because the requirement is to route to the component, not nest it inside the current template.
A shared service also seems possible:
- Store data in the service before navigating
- Read the data from the service in
SecondComponent
Is using a service the usual or most elegant solution, or are there better ways to pass data between routed Angular components with as little code as possible?
Short Answer
By the end of this page, you will understand the main ways to pass data between Angular routed components, when to use each one, and why some methods work better for simple values while others are better for complex objects. You will also learn the trade-offs between route parameters, query parameters, router navigation state, and shared services.
Concept
In Angular, routed components are created by the router, not by a parent component template. That means @Input() binding does not work the same way it does for nested components.
When you navigate to another route, Angular creates that component from the route configuration. Because of that, data must be passed through mechanisms the router understands, or through shared application state.
The common options are:
- Route parameters: good for required, URL-friendly values like IDs
- Query parameters: good for optional filters, search terms, pagination, and small primitive values
- Navigation state: good for temporary data passed during navigation without putting it in the URL
- Shared service: good for complex objects, shared state, or data that multiple components may need
Why this matters:
- URLs should remain meaningful and bookmarkable when possible
- Complex objects usually should not be serialized into the URL
- Routed components should be able to reload correctly if the page is refreshed
- Some data is app state, not route state
A useful rule is:
- If the data identifies what page to show, put it in the route
- If the data changes how the page behaves, query params may fit
- If the data is temporary or complex, use navigation state or a service
For modern Angular applications, the most practical choices are usually:
- Route params for IDs
- Query params for optional URL-visible values
- Shared service or state management for complex objects
- Navigation extras
statefor temporary non-URL data
For example, instead of passing a full property3 object, you often pass its id in the route and let SecondComponent load the full object again. That is usually the cleanest and most reliable approach.
Mental Model
Think of Angular routing like sending someone to a new office.
- Route params are like writing the office number on the address label
- Query params are like adding extra delivery notes
- Navigation state is like handing the courier a private note that is not written on the envelope
- Shared service is like storing a file in a shared cabinet that both offices can access
Now imagine sending a whole chair through the address label. That does not make sense. In the same way, large or complex objects usually do not belong in the URL.
So the mental model is:
- Put small identifying information in the route
- Put optional URL-visible settings in query params
- Put rich shared data in a service or app state
- Use navigation state only for temporary handoff data
Syntax and Examples
1. Route parameters
Use route parameters when the destination component can load what it needs from an ID.
this.router.navigate(['/second', this.property1]);
Route configuration:
const routes: Routes = [
{ path: 'second/:id', component: SecondComponent }
];
Read it in the target component:
import { ActivatedRoute } from '@angular/router';
export class SecondComponent {
constructor(private route: ActivatedRoute) {}
ngOnInit() {
const id = this.route.snapshot.paramMap.get('id');
console.log(id);
}
}
This is best when the route itself should identify the resource.
2. Query parameters
Use query parameters for optional values.
this.router.navigate(['/second'], {
queryParams: {
p1: this.property1,
p2: this.property2
}
});
Read them:
export class SecondComponent {
constructor(private route: ActivatedRoute) {}
ngOnInit() {
const p1 = this.route.snapshot.queryParamMap.get('p1');
const p2 = this.route.snapshot.queryParamMap.get('p2');
console.log(p1, p2);
}
}
This keeps values visible in the URL.
3. Navigation state
Use this for temporary data that should not appear in the URL.
this.router.navigate(['/second'], {
state: {
p1: this.property1,
p2: this.property2,
p3: this.property3
}
});
Read it in the destination:
export class SecondComponent {
data = history.state;
ngOnInit() {
console.log(this.data.p1);
console.log(this.data.p2);
console.log(this.data.p3);
}
}
This is convenient, but remember: it is temporary and may not survive a full refresh the way route parameters do.
4. Shared service
Use a service when passing complex objects or shared state.
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class DataService {
private data: TypeXY | null = null;
setData(value: TypeXY) {
this.data = value;
}
getData(): TypeXY | null {
return this.data;
}
}
Set the data before routing:
export class FirstComponent {
constructor(private router: Router, private dataService: DataService) {}
property3!: TypeXY;
routeWithData() {
this.dataService.setData(this.property3);
this.router.navigate(['/second']);
}
}
Read it in the destination:
export class SecondComponent {
constructor(private dataService: DataService) {}
ngOnInit() {
const value = this.dataService.getData();
console.log(value);
}
}
This is simple and useful for complex data, but it depends on in-memory state.
Step by Step Execution
Consider this example using a route parameter and a service together.
// first.component.ts
routeWithData() {
this.dataService.setData(this.property3);
this.router.navigate(['/second', this.property1]);
}
// second.component.ts
ngOnInit() {
const id = this.route.snapshot.paramMap.get('id');
const item = this.dataService.getData();
console.log('Route ID:', id);
console.log('Shared object:', item);
}
Here is what happens step by step:
- The user clicks the button in
FirstComponent. routeWithData()runs.- The method stores
property3inDataService. - The router navigates to
/second/123ifproperty1is123. - Angular destroys or deactivates
FirstComponent. - Angular creates
SecondComponentfor the new route. - In
ngOnInit(),SecondComponentreads the route paramid. - It also reads the shared object from
DataService. - The component now has both:
- a URL-based identifier
- a complex in-memory object
This mixed approach is common because it keeps the route meaningful while avoiding large objects in the URL.
Real World Use Cases
Passing an ID to load a record
A product list page routes to a product details page:
this.router.navigate(['/products', product.id]);
The details page uses the ID to request fresh data from an API.
Sending search filters in the URL
A reports page stores filters in query params:
this.router.navigate(['/reports'], {
queryParams: { status: 'open', page: 2 }
});
This is useful because the URL can be bookmarked or shared.
Passing temporary form data during navigation
A multi-step form may use navigation state for data collected in the previous step.
this.router.navigate(['/checkout/review'], {
state: { draftOrder: order }
});
Sharing selected objects between pages
An admin dashboard may store a selected object in a service before routing to an editor page.
This avoids putting a large object in the URL and keeps the code simple.
Preserving UI state across sibling routes
A shared service can hold current filters, sort order, selected tab, or wizard progress while users move through routed views.
Real Codebase Usage
In real Angular projects, developers usually avoid passing full objects through the URL.
Common patterns include:
1. Pass only an ID, then fetch data
This is the most common and reliable pattern.
this.router.navigate(['/users', user.id]);
Then in the target component:
const id = this.route.snapshot.paramMap.get('id');
this.userService.getUserById(id!).subscribe();
Why teams prefer it:
- URLs stay clean
- Pages can reload correctly
- Data comes from a single source of truth
2. Use guard clauses when route data is missing
When using service-based state or navigation state, the destination may not have data after refresh.
ngOnInit() {
const data = this.dataService.getData();
if (!data) {
this.router.navigate(['/fallback']);
return;
}
this.item = data;
}
This is a common guard-clause pattern.
3. Combine route params with shared state
Use route params for identity and a service for temporary richer data.
4. Use resolvers for preloading route data
In larger apps, a resolver may load data before the component appears. This is useful when the page depends on route params.
5. Use query params for page state
Filters, tab names, sort order, and pagination often belong in query params because they are user-visible and shareable.
6. Centralize shared state in services
Apps often use injectable services to keep selected entities, session context, or wizard progress available across routed components.
Common Mistakes
Using click instead of (click)
Broken:
<div class="button" click="routeWithData()">Pass data and route</div>
Correct Angular event binding:
<div class="button" (click)="routeWithData()">Pass data and route</div>
Trying to use @Input() with a routed component
Broken idea:
<app-second [p3]="property3"></app-second>
This renders SecondComponent inside the current template. It does not navigate through the router.
Use @Input() only for parent-child component relationships in templates.
Putting complex objects in the URL
Broken approach:
this.router.navigate(['/second', JSON.stringify(this.property3)]);
Problems:
- URLs become messy
- Serialization can fail or be unsafe
- Refreshing and deep linking become harder
Prefer passing an ID or using a service.
Depending only on service state after refresh
If a service only stores data in memory, the data is lost when the page reloads.
const data = this.dataService.getData();
// data may be null after refresh
Avoid this by:
- passing an ID in the route
- reloading data from an API
- storing longer-lived state elsewhere if needed
Using route params for optional UI settings
If data is optional, query params are often a better fit.
Use route params for required path identity, not every small option.
Reading params only once when you need updates
If the same component instance stays active while params change, snapshot may not be enough.
this.route.paramMap.subscribe(params => {
console.log(params.get('id'));
});
Use subscription when the route values can change without recreating the component.
Comparisons
| Approach | Best for | Visible in URL | Works with complex objects | Survives refresh well | Typical use |
|---|---|---|---|---|---|
| Route params | Required identifiers | Yes | No | Yes | /users/42 |
| Query params | Optional settings | Yes | Usually no | Yes | ?page=2&sort=name |
| Navigation state | Temporary navigation data | No | Yes | Not reliably | Passing data during one navigation |
| Shared service | In-memory shared state | No | Yes | No, unless persisted | Selected item, wizard state |
@Input() | Nested component communication | No | Yes | N/A | Parent-child templates |
Route params vs query params
- Route params are part of the path and usually required
- Query params are optional and often used for filtering or UI state
Navigation state vs shared service
- Navigation state is good for a one-time handoff during routing
- Shared service is better when multiple components need the same data or when you want a clearer state container
Shared service vs fetching by ID
- Shared service is convenient for temporary in-memory objects
- Fetching by ID is more robust for refreshes, deep links, and consistency
In production apps, fetching by ID from the route is often the strongest default choice.
Cheat Sheet
Quick reference
Navigate with a route param
this.router.navigate(['/second', id]);
Read a route param
const id = this.route.snapshot.paramMap.get('id');
Navigate with query params
this.router.navigate(['/second'], {
queryParams: { page: 1, filter: 'active' }
});
Read query params
const page = this.route.snapshot.queryParamMap.get('page');
Navigate with temporary state
this.router.navigate(['/second'], {
state: { item: myObject }
});
Read navigation state
const item = history.state.item;
Share data through a service
this.dataService.setData(myObject);
this.router.navigate(['/second']);
const item = this.dataService.getData();
Rules of thumb
- Use route params for IDs and required path data
- Use query params for optional URL-visible values
- Use navigation state for temporary data during one navigation
- Use services for complex shared state
- Do not use
@Input()to pass data to a routed component - Do not put large objects in the URL
Edge cases
- Service state is lost on full browser refresh unless you persist it
- Navigation state is also temporary
- If the destination must always work from a fresh URL, pass an ID and reload data
FAQ
How do I pass data to another component through Angular routing?
Use route params, query params, navigation state, or a shared service depending on the type of data. IDs usually go in the route, while complex objects usually go in a service or navigation state.
Can I pass an object through Angular route parameters?
Technically you can serialize data, but it is usually not a good idea. Complex objects do not belong in the URL in most cases. Pass an ID instead, or use a service.
Should I use @Input() for routed components?
No. @Input() is for components rendered directly in a parent template. Routed components are created by the router.
What is the best way to pass complex data between Angular routes?
A shared service is a common choice for complex objects. If the page must survive refreshes, pass an ID in the route and reload the object from a data source.
Is Angular router navigation state better than a service?
It depends. Navigation state is convenient for one-time temporary data during navigation. A service is better for broader shared state and reuse.
What happens to service-passed data if the page refreshes?
If the service stores data only in memory, the data is lost on refresh. That is why route IDs plus reloading are often more reliable.
When should I use query parameters instead of route parameters?
Use query parameters for optional values like filters, search, sorting, pagination, or tab state. Use route parameters for required path identity.
Mini Project
Description
Build a small Angular flow where a user selects a product in one component and navigates to a details page. The route should pass the product ID in the URL, while a shared service temporarily holds the full selected object. This demonstrates the difference between route-friendly data and complex in-memory data.
Goal
Create a routed navigation flow that passes an ID through the URL and accesses a complex object through a shared Angular service.
Requirements
- Create a
ProductServicethat stores and returns a selected product object. - In the first component, save the selected product to the service before navigating.
- Navigate to the second component using the product ID as a route parameter.
- In the second component, read the route parameter and the stored product.
- Add a fallback message if the service no longer has the product after refresh.
Keep learning
Related questions
@Directive vs @Component in Angular: Differences, Use Cases, and When to Use Each
Learn the difference between @Directive and @Component in Angular, including use cases, examples, and when to choose each.
Accessing Input Value from EventTarget in TypeScript
Learn why EventTarget has no value property in TypeScript and safely read values from HTML input events in Angular applications.
Angular (change) vs (ngModelChange): What’s the Difference?
Learn the difference between Angular (change) and (ngModelChange), when each fires, and which one to use in forms and inputs.