Skip to content

Latest commit

 

History

History

@tanstack/angular-db

Angular hooks for TanStack DB. See TanStack/db for more details.

Installation

npm install @tanstack/angular-db @tanstack/db

Usage

Basic Setup

First, create a collection:

import { createCollection, localOnlyCollectionOptions } from "@tanstack/db"

interface Todo {
  id: number
  text: string
  completed: boolean
  projectID: number
  created_at: Date
}

export const todosCollection = createCollection(
  localOnlyCollectionOptions<Todo>({
    getKey: (todo: Todo) => todo.id,
    initialData: [
      {
        id: 1,
        text: "Learn Angular",
        completed: false,
        projectID: 1,
        created_at: new Date(),
      },
    ],
  })
)

Using injectLiveQuery in Components

Direct Collection Usage

The simplest way to use injectLiveQuery is to pass a collection directly:

import { Component } from "@angular/core"
import { injectLiveQuery } from "@tanstack/angular-db"
import { todosCollection } from "./collections/todos-collection"

@Component({
  selector: "app-all-todos",
  template: `
    @if (allTodos.isReady()) {
      <div>Total todos: {{ allTodos.data().length }}</div>
      @for (todo of allTodos.data(); track todo.id) {
        <div>{{ todo.text }}</div>
      }
    } @else {
      <div>Loading todos...</div>
    }
  `,
})
export class AllTodosComponent {
  // Direct collection usage - gets all items
  allTodos = injectLiveQuery(todosCollection)
}

Static Query Functions

You can create filtered queries using a query function. Note: The query function is evaluated once and is not reactive to signal changes:

import { Component } from "@angular/core"
import { injectLiveQuery } from "@tanstack/angular-db"
import { eq } from "@tanstack/db"
import { todosCollection } from "./collections/todos-collection"

@Component({
  selector: "app-todos",
  template: `
    @if (todoQuery.isReady()) {
      @for (todo of todoQuery.data(); track todo.id) {
        <div class="todo-item">
          {{ todo.text }}
          <button (click)="toggleTodo(todo.id)">
            {{ todo.completed ? "Undo" : "Complete" }}
          </button>
        </div>
      }
    } @else {
      <div>Loading todos...</div>
    }
  `,
})
export class TodosComponent {
  // Static query - filters for incomplete todos
  // This will not react to signal changes within the function
  todoQuery = injectLiveQuery((q) =>
    q
      .from({ todo: todosCollection })
      .where(({ todo }) => eq(todo.completed, false))
  )

  toggleTodo(id: number) {
    todosCollection.utils.begin()
    todosCollection.utils.write({
      type: 'update',
      key: id,
      value: { completed: true }
    })
    todosCollection.utils.commit()
  }
}

Reactive Queries with Parameters

For queries that need to react to component state changes, use the reactive parameters overload:

import { Component, signal } from "@angular/core"
import { injectLiveQuery } from "@tanstack/angular-db"
import { eq } from "@tanstack/db"
import { todosCollection } from "./collections/todos-collection"

@Component({
  selector: "app-project-todos",
  template: `
    <select (change)="selectedProjectId.set(+$any($event).target.value)">
      <option [value]="1">Project 1</option>
      <option [value]="2">Project 2</option>
      <option [value]="3">Project 3</option>
    </select>

    @if (todoQuery.isReady()) {
      <div>Todos for project {{ selectedProjectId() }}:</div>
      @for (todo of todoQuery.data(); track todo.id) {
        <div class="todo-item">
          {{ todo.text }}
        </div>
      }
    } @else {
      <div>Loading todos...</div>
    }
  `,
})
export class ProjectTodosComponent {
  selectedProjectId = signal(1)

  // Reactive query - automatically recreates when selectedProjectId changes
  todoQuery = injectLiveQuery({
    params: () => ({ projectID: this.selectedProjectId() }),
    query: ({ params, q }) =>
      q
        .from({ todo: todosCollection })
        .where(({ todo }) => eq(todo.completed, false))
        .where(({ todo }) => eq(todo.projectID, params.projectID)),
  })
}

Advanced Configuration

You can also pass a full configuration object:

import { Component } from "@angular/core"
import { injectLiveQuery } from "@tanstack/angular-db"
import { eq } from "@tanstack/db"
import { todosCollection } from "./collections/todos-collection"

@Component({
  selector: "app-configured-todos",
  template: `
    @if (todoQuery.isReady()) {
      @for (todo of todoQuery.data(); track todo.id) {
        <div>{{ todo.text }}</div>
      }
    }
  `,
})
export class ConfiguredTodosComponent {
  todoQuery = injectLiveQuery({
    query: (q) =>
      q
        .from({ todo: todosCollection })
        .where(({ todo }) => eq(todo.completed, false))
        .select(({ todo }) => ({
          id: todo.id,
          text: todo.text,
        })),
    startSync: true,
    gcTime: 5000,
  })
}

Important Notes

Reactivity Behavior

  • Direct collection: Automatically reactive to collection changes
  • Static query function: Query is built once and is not reactive to signals read within the function
  • Reactive parameters: Query rebuilds when any signal read in params() changes
  • Collection configuration: Static, not reactive to external signals

Lifecycle Management

  • injectLiveQuery automatically handles subscription cleanup when the component is destroyed
  • Each call to injectLiveQuery creates a new collection instance (no caching/reuse)
  • Collections are started immediately and will sync according to their configuration

Template Usage

Use Angular's new control flow syntax for best performance:

@if (query.isReady()) {
  @for (item of query.data(); track item.id) {
    <div>{{ item.text }}</div>
  }
} @else if (query.isError()) {
  <div>Error loading data</div>
} @else {
  <div>Loading...</div>
}

API

injectLiveQuery()

Angular injection function for TanStack DB live queries. Must be called within an injection context (e.g., component constructor, inject(), or field initializer).

Overloads

// Direct collection - reactive to collection changes
function injectLiveQuery<TResult, TKey, TUtils>(
  collection: Collection<TResult, TKey, TUtils>
): LiveQueryResult<TResult>

// Static query function - NOT reactive to signals within function
function injectLiveQuery<TContext>(
  queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>
): LiveQueryResult<TContext>

// Reactive query with parameters - recreates when params() signals change
function injectLiveQuery<TContext, TParams>(options: {
  params: () => TParams
  query: (args: {
    params: TParams
    q: InitialQueryBuilder
  }) => QueryBuilder<TContext>
}): LiveQueryResult<TContext>

// Collection configuration - static configuration
function injectLiveQuery<TContext>(
  config: LiveQueryCollectionConfig<TContext>
): LiveQueryResult<TContext>

Returns

An object with Angular signals:

  • data: Signal<Array> - Array of query results, automatically updates
  • state: Signal<Map<Key, T>> - Map of results by key, automatically updates
  • collection: Signal - The underlying collection instance
  • status: Signal - Current status ('idle' | 'loading' | 'ready' | 'error' | 'cleaned-up')
  • isLoading: Signal - true when status is 'loading'
  • isReady: Signal - true when status is 'ready'
  • isIdle: Signal - true when status is 'idle'
  • isError: Signal - true when status is 'error'
  • isCleanedUp: Signal - true when status is 'cleaned-up'

Parameters

  • collection - Existing collection to observe directly
  • queryFn - Function that builds a static query using the query builder
  • options.params - Reactive function that returns parameters; triggers query rebuild when accessed signals change
  • options.query - Function that builds a query using parameters and query builder
  • config - Configuration object for creating a live query collection

Requirements

  • Angular 16+ (requires signals support)
  • Must be called within an Angular injection context
  • Automatically handles cleanup when the injector is destroyed