Guides & Concepts

Queries

Query Basics

A query is a declarative dependency on an asynchronous source of data that is tied to a unique key. A query can be used with any Promise based method (including GET and POST methods) to fetch data from a server. If your method modifies data on the server, we recommend using Mutations instead.

To subscribe to a query in your components or services, call with at least:

  • A unique key for the query
  • A function that returns a promise or observable that:
    • Resolves the data, or
    • Throws an error
ts
import { injectQuery } from '@tanstack/angular-query-experimental'

export class TodosComponent {
  info = injectQuery(() => ({ queryKey: ['todos'], queryFn: fetchTodoList }))
}
import { injectQuery } from '@tanstack/angular-query-experimental'

export class TodosComponent {
  info = injectQuery(() => ({ queryKey: ['todos'], queryFn: fetchTodoList }))
}

The unique key you provide is used internally for refetching, caching, and sharing your queries throughout your application.

The query result returned by contains all of the information about the query that you'll need for templating and any other usage of the data:

ts
result = injectQuery(() => ({ queryKey: ['todos'], queryFn: fetchTodoList }))
result = injectQuery(() => ({ queryKey: ['todos'], queryFn: fetchTodoList }))

The object contains a few very important states you'll need to be aware of to be productive. A query can only be in one of the following states at any given moment:

  • or - The query has no data yet
  • or - The query encountered an error
  • or - The query was successful and data is available

Beyond those primary states, more information is available depending on the state of the query:

  • - If the query is in an state, the error is available via the property.
  • - If the query is in an state, the data is available via the property.
  • - In any state, if the query is fetching at any time (including background refetching) will be .

For most queries, it's usually sufficient to check for the state, then the state, then finally, assume that the data is available and render the successful state:

angular-ts
@Component({
  selector: 'todos',
  template: `
    @if (todos.isPending()) {
      <span>Loading...</span>
    } @else if (todos.isError()) {
      <span>Error: {{ todos.error()?.message }}</span>
    } @else {
      <!-- We can assume by this point that status === 'success' -->
      @for (todo of todos.data(); track todo.id) {
        <li>{{ todo.title }}</li>
      } @empty {
        <li>No todos found</li>
      }
    }
  `,
})
export class PostsComponent {
  todos = injectQuery(() => ({
    queryKey: ['todos'],
    queryFn: fetchTodoList,
  }))
}
@Component({
  selector: 'todos',
  template: `
    @if (todos.isPending()) {
      <span>Loading...</span>
    } @else if (todos.isError()) {
      <span>Error: {{ todos.error()?.message }}</span>
    } @else {
      <!-- We can assume by this point that status === 'success' -->
      @for (todo of todos.data(); track todo.id) {
        <li>{{ todo.title }}</li>
      } @empty {
        <li>No todos found</li>
      }
    }
  `,
})
export class PostsComponent {
  todos = injectQuery(() => ({
    queryKey: ['todos'],
    queryFn: fetchTodoList,
  }))
}

If booleans aren't your thing, you can always use the state as well:

angular-ts
@Component({
  selector: 'todos',
  template: `
    @switch (todos.status()) {
      @case ('pending') {
        <span>Loading...</span>
      }
      @case ('error') {
        <span>Error: {{ todos.error()?.message }}</span>
      }
      <!-- also status === 'success', but "else" logic works, too -->
      @default {
        <ul>
          @for (todo of todos.data(); track todo.id) {
            <li>{{ todo.title }}</li>
          } @empty {
            <li>No todos found</li>
          }
        </ul>
      }
    }
  `,
})
class TodosComponent {}
@Component({
  selector: 'todos',
  template: `
    @switch (todos.status()) {
      @case ('pending') {
        <span>Loading...</span>
      }
      @case ('error') {
        <span>Error: {{ todos.error()?.message }}</span>
      }
      <!-- also status === 'success', but "else" logic works, too -->
      @default {
        <ul>
          @for (todo of todos.data(); track todo.id) {
            <li>{{ todo.title }}</li>
          } @empty {
            <li>No todos found</li>
          }
        </ul>
      }
    }
  `,
})
class TodosComponent {}

TypeScript will also narrow the type of correctly if you've checked for and before accessing it.

FetchStatus

In addition to the field, you will also get an additional property with the following options:

  • - The query is currently fetching.
  • - The query wanted to fetch, but it is paused. Read more about this in the Network Mode guide.
  • - The query is not doing anything at the moment.

Why two different states?

Background refetches and stale-while-revalidate logic make all combinations for and possible. For example:

  • a query in status will usually be in fetchStatus, but it could also be in if a background refetch is happening.
  • a query that mounts and has no data will usually be in status and fetchStatus, but it could also be if there is no network connection.

So keep in mind that a query can be in state without actually fetching data. As a rule of thumb:

  • The gives information about the : Do we have any or not?
  • The gives information about the : Is it running or not?