SuperAPI AI Directory

tanstack-angular-db

Frontend & websites

View repository

Updated 10-16 06:24

About this skill

This skill builds on db-core. Read it first for collection setup, query builder, and mutation patterns.

TanStack DB — Angular

Setup

import { Component } from '@angular/core'
import { injectLiveQuery, eq, not } from '@tanstack/angular-db'

@Component({ selector: 'app-todo-list', standalone: true, template: @if (query.isLoading()) { <div>Loading...</div> } @else { <ul> @for (todo of query.data(); track todo.id) { <li>{{ todo.text }}</li> } </ul> } , }) export class TodoListComponent { query = injectLiveQuery((q) => q .from({ todos: todosCollection }) .where(({ todos }) => not(todos.completed)) .orderBy(({ todos }) => todos.created_at, 'asc'), ) }


`@tanstack/angular-db` re-exports everything from `@tanstack/db`.

## Inject Function

### injectLiveQuery

Returns an object with Angular `Signal<T>` properties — call with `()` in templates:

```typescript
// Static query — no reactive dependencies
const query = injectLiveQuery((q) => q.from({ todo: todoCollection }))
// query.data()       → Array<T>
// query.status()     → CollectionStatus | 'disabled'
// query.isLoading(), query.isReady(), query.isError()
// query.isIdle(), query.isCleanedUp()  (seldom used)
// query.state()      → Map<TKey, T>
// query.collection() → Collection | null

// Reactive params — re-runs when params change
const query = injectLiveQuery({
  params: () => ({ minPriority: this.minPriority() }),
  query: ({ params, q }) =>
    q
      .from({ todo: todoCollection })
      .where(({ todo }) => gt(todo.priority, params.minPriority)),
})

// Config object
const query = injectLiveQuery({
  query: (q) => q.from({ todo: todoCollection }),
  gcTime: 60000,
})

// Pre-created collection
const query = injectLiveQuery(preloadedCollection)

// Conditional query — return undefined/null to disable
const query = injectLiveQuery({
  params: () => ({ userId: this.userId() }),
  query: ({ params, q }) => {
    if (!params.userId) return undefined
    return q
      .from({ todo: todoCollection })
      .where(({ todo }) => eq(todo.userId, params.userId))
  },
})

Angular-Specific Patterns

Reactive params with signals

@Component({
  selector: 'app-filtered-todos',
  standalone: true,
  template: `<div>{{ query.data().length }} todos</div>`,
})
export class FilteredTodosComponent {
  minPriority = signal(5)

  query = injectLiveQuery({
    params: () => ({ minPriority: this.minPriority() }),
    query: ({ params, q }) =>
      q
        .from({ todos: todosCollection })
        .where(({ todos }) => gt(todos.priority, params.minPriority)),
  })
}

When params() return value changes, the previous collection is disposed and a new query is created.

Signal inputs (Angular 17+)

@Component({
  selector: 'app-user-todos',
  standalone: true,
  template: `<div>{{ query.data().length }} todos</div>`,
})
export class UserTodosComponent {
  userId = input.required<number>()

  query = injectLiveQuery({
    params: () => ({ userId: this.userId() }),
    query: ({ params, q }) =>
      q
        .from({ todo: todoCollection })
        .where(({ todo }) => eq(todo.userId, params.userId)),
  })
}

Legacy @Input (Angular 16)

export class UserTodosComponent {
  @Input({ required: true }) userId!: number

  query = injectLiveQuery({
    params: () => ({ userId: this.userId }),
    query: ({ params, q }) =>
      q
        .from({ todo: todoCollection })
        .where(({ todo }) => eq(todo.userId, params.userId)),
  })
}

Template syntax

Angular 17+ control flow:

@if (query.isLoading()) {
<div>Loading...</div>
} @else { @for (todo of query.data(); track todo.id) {
<li>{{ todo.text }}</li>
} }

Angular 16 structural directives:

<div *ngIf="query.isLoading()">Loading...</div>
<li *ngFor="let todo of query.data(); trackBy: trackById">{{ todo.text }}</li>

Includes (Hierarchical Data)

When a query uses includes (subqueries in select), each child field is a live Collection by default. Subscribe to it with injectLiveQuery in a child component:

@Component({
  selector: 'app-project-list',
  standalone: true,
  imports: [IssueListComponent],
  template: `
    @for (project of query.data(); track project.id) {
      <div>
        {{ project.name }}
        <app-issue-list [issuesCollection]="project.issues" />
      </div>
    }
  `,
})
export class ProjectListComponent {
  query = injectLiveQuery((q) =>
    q.from({ p: projectsCollection }).select(({ p }) => ({
      id: p.id,
      name: p.name,
      issues: q
        .from({ i: issuesCollection })
        .where(({ i }) => eq(i.projectId, p.id))
        .select(({ i }) => ({ id: i.id, title: i.title })),
    })),
  )
}

// Child component subscribes to the child Collection
@Component({
  selector: 'app-issue-list',
  standalone: true,
  template: `
    @for (issue of query.data(); track issue.id) {
      <li>{{ issue.title }}</li>
    }
  `,
})
export class IssueListComponent {
  issuesCollection = input.required<Collection>()

  query = injectLiveQuery(this.issuesCollection())
}

With toArray(), child results are plain arrays and the parent re-emits on child changes:

import { toArray, eq } from '@tanstack/angular-db'

query = injectLiveQuery((q) =>
  q.from({ p: projectsCollection }).select(({ p }) => ({
    id: p.id,
    name: p.name,
    issues: toArray(
      q
        .from({ i: issuesCollection })
        .where(({ i }) => eq(i.projectId, p.id))
        .select(({ i }) => ({ id: i.id, title: i.title })),
    ),
  })),
)
// project.issues is a plain array — no child component subscription needed

See ../tanstack-db-core-live-queries/SKILL.md for full includes rules (correlation conditions, nested includes, aggregates).

Common Mistakes

CRITICAL Using injectLiveQuery outside injection context

Wrong:

export class TodoComponent {
  ngOnInit() {
    this.query = injectLiveQuery((q) => q.from({ todo: todoCollection }))
  }
}

Correct:

export class TodoComponent {
  query = injectLiveQuery((q) => q.from({ todo: todoCollection }))
}

injectLiveQuery calls assertInInjectionContext internally — it must be called during construction (field initializer or constructor), not in lifecycle hooks.

Source: packages/angular-db/src/index.ts

HIGH Using query function for reactive values instead of params

Wrong:

export class FilteredComponent {
  status = signal('active')

  query = injectLiveQuery((q) =>
    q
      .from({ todo: todoCollection })
      .where(({ todo }) => eq(todo.status, this.status())),
  )
}

Correct:

export class FilteredComponent {
  status = signal('active')

  query = injectLiveQuery({
    params: () => ({ status: this.status() }),
    query: ({ params, q }) =>
      q
        .from({ todo: todoCollection })
        .where(({ todo }) => eq(todo.status, params.status)),
  })
}

The plain query function overload does not track Angular signal reads. Use the params pattern to make reactive values trigger query re-creation.

Source: packages/angular-db/src/index.ts

MEDIUM Forgetting to call signals in templates

Wrong:

<div>{{ query.data.length }}</div>

Correct:

<div>{{ query.data().length }}</div>

All return values are Angular signals. Without (), you get the signal object, not the value.

See also: ../tanstack-db-core-live-queries/SKILL.md — for query builder API.

See also: ../tanstack-db-core-mutations-optimistic/SKILL.md — for mutation patterns.