The Paginator application uses URL query parameters as the single source of truth for application state. This approach enables bookmarking, sharing, and browser navigation while keeping the UI in sync with the URL.
Overview
Query parameters drive the entire application state, including:
state : Selected state filter
pageSize : Number of records per page (10 or 20)
page : Current page number
Why Query Parameters?
Using query parameters for state management provides several benefits:
Bookmarkable Users can bookmark specific filter/page combinations
Shareable URLs can be shared to show exact same view
Browser Navigation Back/forward buttons work as expected
State Persistence Page refresh preserves current state
Query Parameter Structure
The application uses these query parameters:
Parameter Type Default Description statestring | null null State ID to filter cities pageSizenumber 10 Records per page (10 or 20) pagenumber 1 Current page number
Example URLs
# Show all cities, 10 per page, page 1 (defaults)
http://localhost:4200/
# Filter by California, 20 per page, page 3
http://localhost:4200/?state=06&pageSize=20&page=3
# Show all cities, 10 per page, page 5
http://localhost:4200/?page=5
Reading Query Parameters
The HomeComponent reads query parameters in multiple lifecycle hooks:
On Component Initialization
(home.component.ts:32)
ngOnInit (): void {
this . getStates ();
this . watchQueryParams ();
// Store initial query params for use in AfterViewInit
this . route . queryParams . subscribe ( params => {
this . initialQueryParams = params ;
});
}
After View Initialization
(home.component.ts:41)
ngAfterViewInit (): void {
// Extract parameters with defaults
const state = this . initialQueryParams ?. state || null ;
const pageSize = this . initialQueryParams ?. pageSize ? + this . initialQueryParams . pageSize : 10 ;
const page = this . initialQueryParams ?. page ? + this . initialQueryParams . page : 1 ;
// Initialize filters from URL
this . filtersComponent . initFromQueryParams ( state , pageSize );
// Fetch data based on URL parameters
this . getCities ({ state , pageSize , page });
}
The + operator converts string query params to numbers: +params.pageSize
Watching Query Parameter Changes
The watchQueryParams() method (home.component.ts:53) subscribes to query parameter changes:
private watchQueryParams (): void {
this . route . queryParams . subscribe ( params => {
// Extract with defaults
const state = params [ 'state' ] || null ;
const pageSize = params [ 'pageSize' ] ? + params [ 'pageSize' ] : 10 ;
const page = params [ 'page' ] ? + params [ 'page' ] : 1 ;
// Update filter UI to match URL
this . filtersComponent ?. initFromQueryParams ( state , pageSize );
// Fetch data whenever any parameter changes
this . getCities ({ state , pageSize , page });
});
}
Data Flow
Updating Query Parameters
The application updates query parameters through Angular’s Router:
When State Filter Changes
(home.component.ts:94)
onStateChanged ( state : string | null ): void {
const currentQueryParams = { ... this . route . snapshot . queryParams };
this . router . navigate ([], {
queryParams: {
... currentQueryParams ,
state: state || null ,
page: 1 // Reset to first page
},
queryParamsHandling: 'merge'
});
}
When Page Size Changes
(home.component.ts:106)
onPageSizeChanged ( pageSize : number ): void {
const currentQueryParams = { ... this . route . snapshot . queryParams };
this . router . navigate ([], {
queryParams: {
... currentQueryParams ,
pageSize ,
page: 1 // Reset to first page
},
queryParamsHandling: 'merge'
});
}
When Page Changes
(home.component.ts:118)
onPageChanged ( page : number ): void {
const currentQueryParams = { ... this . route . snapshot . queryParams };
this . router . navigate ([], {
queryParams: {
... currentQueryParams ,
page
},
queryParamsHandling: 'merge'
});
}
Key Patterns
Preserve Existing Parameters
Always spread existing query params to preserve other parameters:
const currentQueryParams = { ... this . route . snapshot . queryParams };
this . router . navigate ([], {
queryParams: {
... currentQueryParams , // Keep other params
page: newPage // Update specific param
}
});
Use Query Params Handling
Use queryParamsHandling: 'merge' to merge with existing params:
this . router . navigate ([], {
queryParams: { state: newState },
queryParamsHandling: 'merge' // Merge with existing
});
Always reset to page 1 when filters change:
queryParams : {
... currentQueryParams ,
state : newState ,
page : 1 // Important!
}
Convert Strings to Numbers
Query parameters are always strings, convert when needed:
const pageSize = params [ 'pageSize' ] ? + params [ 'pageSize' ] : 10 ;
const page = params [ 'page' ] ? + params [ 'page' ] : 1 ;
Complete Flow Example
Here’s the complete flow when a user changes the state filter:
User Selects State
User selects “California” in the dropdown.
Form Control Emits Change
The stateControl.valueChanges observable emits the new value (filters.component.ts:23): this . stateControl . valueChanges . subscribe ( value => {
this . stateChanged . emit ( value ?? null );
});
Parent Handler Updates URL
The onStateChanged handler updates query parameters (home.component.ts:94): onStateChanged ( state : string | null ): void {
this . router . navigate ([], {
queryParams: { state , page: 1 }
});
}
URL Changes
Browser URL updates to: ?state=06&pageSize=10&page=1
Query Params Observable Fires
The watchQueryParams subscription receives the new params (home.component.ts:53): this . route . queryParams . subscribe ( params => {
const state = params [ 'state' ] || null ;
// ...
});
UI Syncs with URL
Filters component updates to match URL: this . filtersComponent ?. initFromQueryParams ( state , pageSize );
Data Fetches
New data is fetched with the updated parameters: this . getCities ({ state , pageSize , page });
View Updates
The component renders the filtered data.
Avoiding Infinite Loops
When initializing form controls from query params, use { emitEvent: false }:
initFromQueryParams ( state : string | null , pageSize : number ) {
// Don't emit events during initialization
this . stateControl . setValue ( state ?? '' , { emitEvent: false });
this . pageSizeControl . setValue ( pageSize ?? 10 , { emitEvent: false });
}
Without { emitEvent: false }, setting form values would trigger valueChanges, which would update query params, which would trigger queryParams subscription, creating an infinite loop.
Type Safety
The CityFilters interface (types/location.ts:37) ensures type safety:
export interface CityFilters {
state ?: string ;
pageSize ?: number ;
page ?: number ;
}
private getCities ( filters : CityFilters ): void {
this . locationService . getCities ( filters ). subscribe ( /* ... */ );
}
Best Practices
Always provide default values when reading query parameters: const page = params [ 'page' ] ? + params [ 'page' ] : 1 ;
const pageSize = params [ 'pageSize' ] ? + params [ 'pageSize' ] : 10 ;
Preserve Existing Parameters
Spread existing query params when updating: const currentQueryParams = { ... this . route . snapshot . queryParams };
queryParams : { ... currentQueryParams , page : newPage }
Reset Pagination on Filters
Use queryParamsHandling: 'merge' to preserve other parameters: this . router . navigate ([], {
queryParams: { page },
queryParamsHandling: 'merge'
});
Query params are strings, convert explicitly: const page = + params [ 'page' ]; // Convert to number
Use { emitEvent: false } when syncing UI with URL: this . control . setValue ( value , { emitEvent: false });
Testing Query Parameters
You can manually test query parameters by editing the URL:
# Test different states
http://localhost:4200/?state=06
http://localhost:4200/?state=36
# Test different page sizes
http://localhost:4200/?pageSize=20
# Test different pages
http://localhost:4200/?page=5
# Test combinations
http://localhost:4200/?state=06&pageSize=20&page=3
# Test edge cases
http://localhost:4200/?page=999 # Should handle gracefully
http://localhost:4200/?pageSize=50 # Should default to 10 or 20
Debugging Query Parameters
Add logging to track query parameter changes:
private watchQueryParams (): void {
this . route . queryParams . subscribe ( params => {
console . log ( 'Query params changed:' , params );
const state = params [ 'state' ] || null ;
const pageSize = params [ 'pageSize' ] ? + params [ 'pageSize' ] : 10 ;
const page = params [ 'page' ] ? + params [ 'page' ] : 1 ;
console . log ( 'Parsed values:' , { state , pageSize , page });
this . getCities ({ state , pageSize , page });
});
}
Next Steps
Filtering Data Learn how filters integrate with query parameters
Pagination Understand how pagination uses query parameters