Skip to main content

Overview

Lazy queries allow you to trigger data fetching on demand, rather than automatically on component mount. This is useful for:
  • Loading data in response to user actions (button clicks, form submissions)
  • Fetching nested or relational data
  • Implementing search or autocomplete features
  • Deferring expensive queries until needed

Defining a Lazy Query

Lazy query hooks are auto-generated from the same query endpoints as regular queries:
import { createApi, fetchBaseQuery } from 'ngrx-rtk-query';

export interface Post {
  id: number;
  name: string;
}

export const postsApi = createApi({
  baseQuery: fetchBaseQuery({ baseUrl: 'http://api.localhost.com' }),
  endpoints: (build) => ({
    getPost: build.query<Post, number>({
      query: (id) => `/posts/${id}`,
    }),
  }),
});

// Both hooks are generated from the same endpoint
export const {
  useGetPostQuery,      // Regular query (auto-fetches)
  useLazyGetPostQuery,  // Lazy query (manual trigger)
} = postsApi;
Every query endpoint automatically generates both a useXxxQuery and a useLazyXxxQuery hook.

Basic Usage

A lazy query hook returns a trigger function with query state attached:
import { Component } from '@angular/core';
import { useLazyGetPostQuery } from './api';

@Component({
  selector: 'app-post-loader',
  standalone: true,
  template: `
    <button (click)="loadPost(1)">Load Post</button>
    
    @if (postQuery.isLoading()) {
      <p>Loading...</p>
    }
    @if (postQuery.data(); as post) {
      <div>{{ post.name }}</div>
    }
  `,
})
export class PostLoaderComponent {
  postQuery = useLazyGetPostQuery();
  
  loadPost(id: number) {
    this.postQuery(id);
  }
}

Lazy Query Structure

The lazy query hook returns a trigger function with signals attached:
const trigger = useLazyGetPostQuery();

// Trigger function - call to start the query
trigger(postId);

// Signal properties - access like regular query
trigger.data();
trigger.isLoading();
trigger.isError();
trigger.error();
trigger.lastArg();  // The last argument passed to trigger

// Utility methods
trigger.reset();    // Clear query cache
Unlike regular queries, lazy queries return the trigger function itself, with signals attached as properties.

Query State Properties

Lazy queries provide the same state properties as regular queries:
PropertyTypeDescription
data()T | undefinedThe latest successfully fetched data
currentData()T | undefinedThe data for the current query args
isLoading()booleantrue during initial fetch with no cached data
isFetching()booleantrue whenever a request is in flight
isSuccess()booleantrue when data is available
isError()booleantrue if the query resulted in an error
error()SerializedError | FetchBaseQueryErrorError object if query failed
lastArg()ArgType | undefinedThe last argument passed to the trigger
reset()() => voidClear the query cache

Trigger Options

The trigger function accepts an optional second argument for request options:
export class PostLoaderComponent {
  postQuery = useLazyGetPostQuery();
  
  loadPost(id: number) {
    this.postQuery(id, {
      preferCacheValue: true  // Use cached value if available
    });
  }
}

preferCacheValue

When preferCacheValue: true, the query will use cached data if available instead of making a new request:
loadPost(id: number) {
  // First call: makes request
  this.postQuery(id, { preferCacheValue: true });
  
  // Second call with same id: uses cache
  this.postQuery(id, { preferCacheValue: true });
}
preferCacheValue defaults to false. Set it to true to prevent duplicate requests.

Unwrapping Promises

Use .unwrap() to access the resolved data or handle errors:
export class PostLoaderComponent {
  postQuery = useLazyGetPostQuery();
  
  loadPost(id: number) {
    this.postQuery(id)
      .unwrap()
      .then((post) => {
        console.log('Post loaded:', post);
      })
      .catch((error) => {
        console.error('Failed to load post:', error);
      });
  }
}

Async/Await Pattern

async loadPost(id: number) {
  try {
    const post = await this.postQuery(id).unwrap();
    console.log('Post loaded:', post);
  } catch (error) {
    console.error('Failed to load post:', error);
  }
}

Lazy Query Options

Configure query behavior with options:
export class PostLoaderComponent {
  postQuery = useLazyGetPostQuery({
    pollingInterval: 5000,           // Poll every 5 seconds after trigger
    refetchOnFocus: true,            // Refetch when window gains focus
    refetchOnReconnect: true,        // Refetch when connection restored
    selectFromResult: ({ data, isLoading }) => ({ data, isLoading }),
  });
}

Signal or Function Options

Options can be signals or functions:
export class PostLoaderComponent {
  enablePolling = signal(false);
  
  postQuery = useLazyGetPostQuery(
    () => ({
      pollingInterval: this.enablePolling() ? 5000 : 0,
    })
  );
}

Resetting Query State

Use the reset() method to clear the query cache:
@Component({
  template: `
    <button (click)="loadPost(1)">Load Post</button>
    <button (click)="postQuery.reset()">Clear</button>
    
    @if (postQuery.data(); as post) {
      <div>{{ post.name }}</div>
    }
  `
})
export class PostLoaderComponent {
  postQuery = useLazyGetPostQuery();
  
  loadPost(id: number) {
    this.postQuery(id);
  }
}
Calling reset() removes the query from the cache. The next trigger will make a fresh request.

Common Use Cases

User-Triggered Data Loading

@Component({
  template: `
    <button (click)="loadUserDetails()">Load My Details</button>
    @if (userQuery.data(); as user) {
      <div>{{ user.name }}</div>
    }
  `
})
export class UserProfileComponent {
  userQuery = useLazyGetUserQuery();
  userId = signal(1);
  
  loadUserDetails() {
    this.userQuery(this.userId());
  }
}

Nested/Relational Data

import { Component, OnInit, input } from '@angular/core';

export interface Character {
  id: number;
  name: string;
  currentLocation: number;
}

@Component({
  template: `
    <h2>{{ character().name }}</h2>
    @if (locationQuery.data(); as location) {
      <p>Location: {{ location.name }}</p>
    }
  `
})
export class CharacterCardComponent implements OnInit {
  character = input.required<Character>();
  locationQuery = useLazyGetLocationQuery();
  
  ngOnInit() {
    // Load location when component initializes
    this.locationQuery(
      this.character().currentLocation,
      { preferCacheValue: true }
    );
  }
}
For relational data, consider using regular queries with skipToken for a more declarative approach:
locationQuery = useGetLocationQuery(
  () => this.character()?.currentLocation ?? skipToken
);

Search/Autocomplete

@Component({
  template: `
    <input 
      type="text" 
      (input)="search($event)" 
      placeholder="Search posts..." />
    
    @if (searchQuery.isFetching()) {
      <p>Searching...</p>
    }
    @if (searchQuery.data(); as results) {
      @for (result of results; track result.id) {
        <div>{{ result.name }}</div>
      }
    }
  `
})
export class SearchComponent {
  searchQuery = useLazySearchPostsQuery();
  
  search(event: Event) {
    const query = (event.target as HTMLInputElement).value;
    if (query.length > 2) {
      this.searchQuery(query);
    }
  }
}

TypeScript Type Signatures

Lazy Query Hook Type

type UseLazyQuery<ResultType, ArgType> = (
  options?: UseQueryOptions<ResultType> | Signal<UseQueryOptions<ResultType>> | (() => UseQueryOptions<ResultType>)
) => LazyQueryTrigger<ResultType, ArgType>;

type LazyQueryTrigger<ResultType, ArgType> = {
  // Trigger function
  (arg: ArgType, options?: { preferCacheValue?: boolean }): QueryActionCreatorResult<ResultType>;
  
  // Signal properties
  data: Signal<ResultType | undefined>;
  currentData: Signal<ResultType | undefined>;
  error: Signal<SerializedError | FetchBaseQueryError | undefined>;
  isUninitialized: Signal<boolean>;
  isLoading: Signal<boolean>;
  isFetching: Signal<boolean>;
  isSuccess: Signal<boolean>;
  isError: Signal<boolean>;
  
  // Utility properties
  lastArg: Signal<ArgType | undefined>;
  reset: () => void;
};

Best Practices

// ✅ Good - Prevents duplicate requests
export class CharacterCardComponent implements OnInit {
  character = input.required<Character>();
  locationQuery = useLazyGetLocationQuery();
  
  ngOnInit() {
    this.locationQuery(
      this.character().currentLocation,
      { preferCacheValue: true }  // Use cache if available
    );
  }
}

Comparison: Regular vs Lazy Queries

FeatureRegular QueryLazy Query
Fetches on mount✅ Yes❌ No
Manual triggerVia refetch()Via trigger function
Return typeSignal objectTrigger function with signals
Use caseDeclarative, auto-fetchImperative, on-demand
lastArg property❌ No✅ Yes
reset() method❌ No✅ Yes

Build docs developers (and LLMs) love