Overview
ngrx-rtk-query hooks accept signals, functions, or static values as parameters. This makes them perfect for working with Angular’s signal inputs, especially with router parameters.
Queries automatically react to signal input changes, refetching data when the input value updates.
Basic Usage
Define the API endpoint
import { createApi , fetchBaseQuery } from 'ngrx-rtk-query' ;
export interface Post {
id : number ;
name : string ;
content : string ;
}
export const postsApi = createApi ({
baseQuery: fetchBaseQuery ({ baseUrl: 'http://api.localhost.com' }),
endpoints : ( build ) => ({
getPost: build . query < Post , number >({
query : ( id ) => `/posts/ ${ id } ` ,
providesTags : ( result , error , id ) => [{ type: 'Posts' , id }],
}),
}),
});
export const { useGetPostQuery } = postsApi ;
Use with signal input
post-details.component.ts
import { Component , input , numberAttribute } from '@angular/core' ;
import { useGetPostQuery } from './api' ;
@ Component ({
selector: 'app-post-details' ,
template: `
<h1>{{ postQuery.data()?.name }}</h1>
@if (postQuery.isLoading()) {
<p>Loading...</p>
}
@if (postQuery.data(); as post) {
<div>{{ post.content }}</div>
}
` ,
})
export class PostDetailsComponent {
// Signal input from router
id = input . required ({ transform: numberAttribute });
// Query automatically refetches when id changes
postQuery = useGetPostQuery ( this . id );
}
Configure route
import { Route } from '@angular/router' ;
export const appRoutes : Route [] = [
{
path: 'posts/:id' ,
loadComponent : () => import ( './post-details.component' )
. then (( c ) => c . PostDetailsComponent ),
},
];
When the route parameter changes (e.g., navigating from /posts/1 to /posts/2), the query automatically refetches with the new ID.
Enable withComponentInputBinding() to automatically bind route parameters to component inputs:
import { provideRouter , withComponentInputBinding } from '@angular/router' ;
export const appConfig : ApplicationConfig = {
providers: [
provideRouter (
appRoutes ,
withComponentInputBinding (), // Enable automatic binding
),
],
};
Signal Parameter Patterns
Direct Signal
Pass a signal directly to the hook:
id = input . required < number >();
postQuery = useGetPostQuery ( this . id );
Function (Computed)
Use a function for derived values:
id = input . required < number >();
offset = input < number >( 0 );
// Derived parameter
postQuery = useGetPostQuery (() => this . id () + this . offset ());
With skipToken
Conditionally skip queries when a signal is undefined:
import { skipToken } from 'ngrx-rtk-query' ;
character = input < Character | undefined >();
// Only query when character exists
locationQuery = useGetLocationQuery (
() => this . character ()?. currentLocation ?? skipToken
);
Without skipToken, the query would be called with undefined, potentially causing errors.
Handle optional inputs gracefully with skipToken:
Optional with Default
Optional with Fallback
Required Transform
userId = input < number >(); // Optional, defaults to undefined
userQuery = useGetUserQuery (
() => this . userId () ?? skipToken
);
userId = input < number >();
// Use current user ID as fallback
userQuery = useGetUserQuery (
() => this . userId () ?? this . currentUserId ()
);
// Transform string to number, required
id = input . required ({ transform: numberAttribute });
postQuery = useGetPostQuery ( this . id );
Combine multiple signal inputs in a query:
import { Component , input } from '@angular/core' ;
import { useGetPostsQuery } from './api' ;
@ Component ({
template: `
@for (post of postsQuery.data(); track post.id) {
<div>{{ post.name }}</div>
}
` ,
})
export class PostsListComponent {
page = input < number >( 1 );
limit = input < number >( 10 );
search = input < string >( '' );
// Combine multiple signals
postsQuery = useGetPostsQuery (() => ({
page: this . page (),
limit: this . limit (),
search: this . search (),
}));
}
Query Options with Signals
Signal parameters work with query options:
id = input . required < number >();
postQuery = useGetPostQuery ( this . id , {
pollingInterval: 5000 , // Poll every 5 seconds
skip: false ,
refetchOnMountOrArgChange: true ,
});
Dynamic Options
Options can also be signals or functions:
id = input . required < number >();
pollingEnabled = input < boolean >( false );
postQuery = useGetPostQuery (
this . id ,
() => ({
pollingInterval: this . pollingEnabled () ? 5000 : 0 ,
})
);
Skip Queries Conditionally
Use the skip option to prevent queries from running:
With skip option
With skipToken
id = input . required < number >();
enabled = input < boolean >( false );
// Only query when enabled
postQuery = useGetPostQuery (
this . id ,
() => ({ skip: ! this . enabled () })
);
skipToken is preferred when the parameter itself is conditional. Use skip option when you want to control execution based on other state.
Nested/Relational Data
Load related data based on signal inputs:
import { Component , input } from '@angular/core' ;
import { useGetCharacterQuery , useGetLocationQuery } from './api' ;
@ Component ({
template: `
<h2>{{ characterQuery.data()?.name }}</h2>
<p>Location: {{ locationQuery.data()?.name }}</p>
` ,
})
export class CharacterCardComponent {
characterId = input . required < number >();
// Load character
characterQuery = useGetCharacterQuery ( this . characterId );
// Load character's location when character data is available
locationQuery = useGetLocationQuery (
() => this . characterQuery . data ()?. currentLocation ?? skipToken
);
}
Fine-Grained Reactivity
ngrx-rtk-query provides fine-grained signal access for optimal change detection:
@ Component ({
template: `
@if (postQuery.isLoading()) {
<p>Loading...</p>
}
@if (postQuery.data(); as post) {
<h1>{{ post.name }}</h1>
}
` ,
})
export class PostComponent {
postQuery = useGetPostQuery ( 1 );
// Template only re-renders when isLoading or data changes
// Not when other properties like isFetching change
}
@ Component ({
template: `
@if (postQuery().isLoading) {
<p>Loading...</p>
}
@if (postQuery().data; as post) {
<h1>{{ post.name }}</h1>
}
` ,
})
export class PostComponent {
postQuery = useGetPostQuery ( 1 );
// Template re-renders when ANY property changes
// Less efficient
}
Use postQuery.property() instead of postQuery().property for better performance.
Combine polling with signal inputs for real-time data:
import { Component , input , numberAttribute } from '@angular/core' ;
import { useGetPostQuery } from './api' ;
@ Component ({
template: `
<h1>{{ postQuery.data()?.name }}</h1>
@if (postQuery.isFetching()) {
<span class="badge">Updating...</span>
}
<button (click)="postQuery.refetch()">Refresh Now</button>
` ,
})
export class PostDetailsComponent {
id = input . required ({ transform: numberAttribute });
postQuery = useGetPostQuery ( this . id , {
pollingInterval: 5000 , // Poll every 5 seconds
});
}
Advanced Patterns
Debounced Search
Debounce signal inputs for search queries:
import { Component , signal , effect } from '@angular/core' ;
import { useGetPostsQuery } from './api' ;
@ Component ({
template: `
<input
[ngModel]="searchInput()"
(ngModelChange)="searchInput.set($event)"
placeholder="Search posts..."
/>
@for (post of postsQuery.data(); track post.id) {
<div>{{ post.name }}</div>
}
` ,
})
export class SearchComponent {
searchInput = signal ( '' );
debouncedSearch = signal ( '' );
#debounceTimer ?: ReturnType < typeof setTimeout >;
constructor () {
effect (() => {
const value = this . searchInput ();
clearTimeout ( this . #debounceTimer );
this . #debounceTimer = setTimeout (() => {
this . debouncedSearch . set ( value );
}, 300 );
});
}
postsQuery = useGetPostsQuery (
() => ({ search: this . debouncedSearch () })
);
}
Dependent Queries
Chain queries based on previous results:
import { Component , input } from '@angular/core' ;
import { useGetUserQuery , useGetPostsQuery } from './api' ;
import { skipToken } from 'ngrx-rtk-query' ;
@ Component ({
template: `
<h2>{{ userQuery.data()?.name }}'s Posts</h2>
@for (post of postsQuery.data(); track post.id) {
<div>{{ post.title }}</div>
}
` ,
})
export class UserPostsComponent {
userId = input . required < number >();
// First query: load user
userQuery = useGetUserQuery ( this . userId );
// Second query: load user's posts (depends on first query)
postsQuery = useGetPostsQuery (
() => this . userQuery . data ()?. id ?? skipToken
);
}
Test components with signal inputs:
import { ComponentFixture , TestBed } from '@angular/core/testing' ;
import { PostDetailsComponent } from './post-details.component' ;
import { provideStoreApi } from 'ngrx-rtk-query' ;
import { postsApi } from './api' ;
describe ( 'PostDetailsComponent' , () => {
let component : PostDetailsComponent ;
let fixture : ComponentFixture < PostDetailsComponent >;
beforeEach (() => {
TestBed . configureTestingModule ({
imports: [ PostDetailsComponent ],
providers: [ provideStore (), provideStoreApi ( postsApi )],
});
fixture = TestBed . createComponent ( PostDetailsComponent );
component = fixture . componentInstance ;
// Set input value
fixture . componentRef . setInput ( 'id' , 1 );
fixture . detectChanges ();
});
it ( 'should load post data' , async () => {
await fixture . whenStable ();
expect ( component . postQuery . data ()). toBeDefined ();
});
it ( 'should refetch when id changes' , async () => {
await fixture . whenStable ();
const firstPost = component . postQuery . data ();
// Change input
fixture . componentRef . setInput ( 'id' , 2 );
fixture . detectChanges ();
await fixture . whenStable ();
expect ( component . postQuery . data ()). not . toBe ( firstPost );
});
});
Best Practices
Use signal inputs for route parameters
Enable withComponentInputBinding() and use input() instead of ActivatedRoute.
Prefer skipToken for optional parameters
Use skipToken instead of passing undefined to prevent errors.
Use fine-grained signals
Access properties as query.data() instead of query().data for better performance.
Transform route parameters
Use numberAttribute or custom transforms for type safety: id = input . required ({ transform: numberAttribute });
Combine signals in functions
When using multiple signals, wrap them in a function: query = useGetDataQuery (() => ({ id: this . id (), type: this . type () }));
Troubleshooting
Ensure you’re passing a signal or function, not a static value.
// ❌ Wrong - static value
postQuery = useGetPostQuery ( this . id ());
// ✅ Correct - signal
postQuery = useGetPostQuery ( this . id );
// ✅ Also correct - function
postQuery = useGetPostQuery (() => this . id ());
Route parameters not binding
Ensure withComponentInputBinding() is configured:
provideRouter ( appRoutes , withComponentInputBinding ());
Use the correct attribute transformer:
import { numberAttribute , booleanAttribute } from '@angular/core' ;
id = input . required ({ transform: numberAttribute });
enabled = input ({ transform: booleanAttribute });