skills/tanstack/db/angular-db

angular-db

Installation
SKILL.md

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:

// 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>

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: db-core/live-queries/SKILL.md — for query builder API.

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

Weekly Installs
1
Repository
tanstack/db
GitHub Stars
3.7K
First Seen
Mar 25, 2026
Installed on
amp1
cline1
opencode1
cursor1
kimi-cli1
warp1