CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-tanstack--angular-query-experimental

Signals for managing, caching and syncing asynchronous and remote data in Angular

Pending
Overview
Eval results
Files

multi-query-operations.mddocs/

Multi-Query Operations

Functions for handling multiple queries simultaneously with type-safe results and combined operations using Angular signals.

Capabilities

Inject Queries

Handles multiple queries at once with type-safe results and optional result combination.

/**
 * Injects multiple queries and returns their combined results.
 * @param config - Configuration object with queries array and optional combine function
 * @param injector - Optional custom injector
 * @returns Signal containing combined results from all queries
 */
function injectQueries<T extends Array<any>, TCombinedResult = QueriesResults<T>>(
  config: {
    queries: Signal<[...QueriesOptions<T>]>;
    combine?: (result: QueriesResults<T>) => TCombinedResult;
  },
  injector?: Injector
): Signal<TCombinedResult>;

Usage Examples:

import { injectQueries } from "@tanstack/angular-query-experimental";
import { Component, inject, signal } from "@angular/core";
import { HttpClient } from "@angular/common/http";

@Component({
  selector: 'app-dashboard',
  template: `
    <div class="dashboard">
      <div *ngIf="dashboardData.isLoading" class="loading">
        Loading dashboard...
      </div>
      
      <div *ngIf="dashboardData.hasError" class="error">
        Some data failed to load
      </div>
      
      <div *ngIf="dashboardData.data" class="dashboard-content">
        <div class="user-info">
          <h2>{{ dashboardData.data.user?.name }}</h2>
          <p>{{ dashboardData.data.user?.email }}</p>
        </div>
        
        <div class="stats">
          <div class="stat-item">
            <label>Posts</label>
            <span>{{ dashboardData.data.posts?.length || 0 }}</span>
          </div>
          <div class="stat-item">
            <label>Notifications</label>
            <span>{{ dashboardData.data.notifications?.length || 0 }}</span>
          </div>
        </div>
      </div>
    </div>
  `
})
export class DashboardComponent {
  #http = inject(HttpClient);
  
  // Multiple queries with combined result
  private queries = signal([
    {
      queryKey: ['user'] as const,
      queryFn: () => this.#http.get<User>('/api/user')
    },
    {
      queryKey: ['posts'] as const,
      queryFn: () => this.#http.get<Post[]>('/api/posts')
    },
    {
      queryKey: ['notifications'] as const,
      queryFn: () => this.#http.get<Notification[]>('/api/notifications')
    }
  ]);
  
  dashboardData = injectQueries({
    queries: this.queries,
    combine: (results) => {
      const [userResult, postsResult, notificationsResult] = results;
      
      return {
        isLoading: results.some(result => result.isPending),
        hasError: results.some(result => result.isError),
        data: results.every(result => result.isSuccess) ? {
          user: userResult.data,
          posts: postsResult.data,
          notifications: notificationsResult.data
        } : null
      };
    }
  });
}

interface User {
  id: number;
  name: string;
  email: string;
}

interface Post {
  id: number;
  title: string;
  content: string;
}

interface Notification {
  id: number;
  message: string;
  read: boolean;
}

Inject Mutation State

Tracks the state of all mutations across the application.

/**
 * Injects a signal that tracks the state of all mutations.
 * @param injectMutationStateFn - A function that returns mutation state options
 * @param options - Additional configuration including custom injector
 * @returns The signal that tracks the state of all mutations
 */
function injectMutationState<TResult = MutationState>(
  injectMutationStateFn?: () => MutationStateOptions<TResult>,
  options?: InjectMutationStateOptions
): Signal<Array<TResult>>;

interface MutationStateOptions<TResult = MutationState> {
  /** Filters to apply to mutations */
  filters?: MutationFilters;
  /** Function to transform each mutation state */
  select?: (mutation: Mutation) => TResult;
}

interface InjectMutationStateOptions {
  /** The Injector in which to create the mutation state signal */
  injector?: Injector;
}

interface MutationState {
  context: unknown;
  data: unknown;
  error: unknown;
  failureCount: number;
  failureReason: unknown;
  isPaused: boolean;
  status: 'idle' | 'pending' | 'error' | 'success';
  variables: unknown;
  submittedAt: number;
}

Usage Examples:

import { injectMutationState } from "@tanstack/angular-query-experimental";
import { Component, computed } from "@angular/core";

@Component({
  selector: 'app-operations-monitor',
  template: `
    <div class="operations-monitor">
      <h3>Active Operations</h3>
      
      <div *ngIf="activeMutations().length === 0" class="no-operations">
        No active operations
      </div>
      
      <div 
        *ngFor="let mutation of activeMutations()" 
        class="operation-item"
        [ngClass]="'status-' + mutation.status"
      >
        <span class="operation-name">{{ mutation.name }}</span>
        <span class="operation-status">{{ mutation.status }}</span>
        <div *ngIf="mutation.error" class="operation-error">
          {{ mutation.error }}
        </div>
      </div>
      
      <div class="summary">
        <div>Pending: {{ pendingCount() }}</div>
        <div>Failed: {{ failedCount() }}</div>
        <div>Successful: {{ successCount() }}</div>
      </div>
    </div>
  `
})
export class OperationsMonitorComponent {
  // All mutation states with custom selection
  allMutations = injectMutationState(() => ({
    select: (mutation) => ({
      id: mutation.mutationId,
      name: this.getMutationName(mutation),
      status: mutation.state.status,
      error: mutation.state.error?.message || null,
      variables: mutation.state.variables,
      submittedAt: mutation.state.submittedAt
    })
  }));
  
  // Only active (pending) mutations
  activeMutations = injectMutationState(() => ({
    filters: { status: 'pending' },
    select: (mutation) => ({
      name: this.getMutationName(mutation),
      status: mutation.state.status,
      error: mutation.state.error?.message || null
    })
  }));
  
  // Computed summary statistics
  pendingCount = computed(() => 
    this.allMutations().filter(m => m.status === 'pending').length
  );
  
  failedCount = computed(() => 
    this.allMutations().filter(m => m.status === 'error').length
  );
  
  successCount = computed(() => 
    this.allMutations().filter(m => m.status === 'success').length
  );
  
  private getMutationName(mutation: any): string {
    const key = mutation.options.mutationKey?.[0];
    return typeof key === 'string' ? key : 'Unknown Operation';
  }
}

@Component({
  selector: 'app-recent-changes',
  template: `
    <div class="recent-changes">
      <h4>Recent Changes</h4>
      <div 
        *ngFor="let change of recentChanges()" 
        class="change-item"
      >
        <span class="change-type">{{ change.type }}</span>
        <span class="change-time">{{ formatTime(change.time) }}</span>
        <div class="change-details">{{ change.details }}</div>
      </div>
    </div>
  `
})
export class RecentChangesComponent {
  // Track successful mutations for audit trail
  recentChanges = injectMutationState(() => ({
    filters: { status: 'success' },
    select: (mutation) => ({
      type: this.getMutationType(mutation),
      time: mutation.state.submittedAt,
      details: this.getMutationDetails(mutation),
      data: mutation.state.data
    })
  }));
  
  private getMutationType(mutation: any): string {
    const key = mutation.options.mutationKey?.[0];
    if (typeof key === 'string') {
      if (key.includes('create')) return 'Created';
      if (key.includes('update')) return 'Updated';
      if (key.includes('delete')) return 'Deleted';
    }
    return 'Modified';
  }
  
  private getMutationDetails(mutation: any): string {
    const variables = mutation.state.variables;
    if (variables && typeof variables === 'object') {
      return JSON.stringify(variables, null, 2);
    }
    return 'No details available';
  }
  
  formatTime(timestamp: number): string {
    return new Date(timestamp).toLocaleTimeString();
  }
}

Type Definitions

Comprehensive type definitions for multi-query operations.

/**
 * QueriesOptions reducer recursively unwraps function arguments to infer/enforce type param
 */
type QueriesOptions<
  T extends Array<any>,
  TResult extends Array<any> = [],
  TDepth extends ReadonlyArray<number> = []
> = TDepth['length'] extends 20 // MAXIMUM_DEPTH
  ? Array<QueryObserverOptionsForCreateQueries>
  : T extends []
    ? []
    : T extends [infer Head]
      ? [...TResult, GetOptions<Head>]
      : T extends [infer Head, ...infer Tail]
        ? QueriesOptions<[...Tail], [...TResult, GetOptions<Head>], [...TDepth, 1]>
        : ReadonlyArray<unknown> extends T
          ? T
          : T extends Array<QueryObserverOptionsForCreateQueries<infer TQueryFnData, infer TError, infer TData, infer TQueryKey>>
            ? Array<QueryObserverOptionsForCreateQueries<TQueryFnData, TError, TData, TQueryKey>>
            : Array<QueryObserverOptionsForCreateQueries>;

/**
 * QueriesResults reducer recursively maps type param to results
 */
type QueriesResults<
  T extends Array<any>,
  TResult extends Array<any> = [],
  TDepth extends ReadonlyArray<number> = []
> = TDepth['length'] extends 20 // MAXIMUM_DEPTH
  ? Array<QueryObserverResult>
  : T extends []
    ? []
    : T extends [infer Head]
      ? [...TResult, GetResults<Head>]
      : T extends [infer Head, ...infer Tail]
        ? QueriesResults<[...Tail], [...TResult, GetResults<Head>], [...TDepth, 1]>
        : T extends Array<QueryObserverOptionsForCreateQueries<infer TQueryFnData, infer TError, infer TData, any>>
          ? Array<QueryObserverResult<unknown extends TData ? TQueryFnData : TData, unknown extends TError ? DefaultError : TError>>
          : Array<QueryObserverResult>;

Advanced Usage Patterns

Dynamic Query Arrays

@Component({})
export class DynamicQueriesComponent {
  #http = inject(HttpClient);
  userIds = signal([1, 2, 3, 4, 5]);
  
  // Dynamic queries based on user IDs
  userQueries = computed(() => {
    return this.userIds().map(id => ({
      queryKey: ['user', id] as const,
      queryFn: () => this.#http.get<User>(`/api/users/${id}`)
    }));
  });
  
  users = injectQueries({
    queries: this.userQueries,
    combine: (results) => {
      return {
        users: results.map(result => result.data).filter(Boolean),
        loading: results.some(result => result.isPending),
        errors: results.filter(result => result.isError).map(result => result.error)
      };
    }
  });
  
  addUser(id: number) {
    this.userIds.update(ids => [...ids, id]);
  }
  
  removeUser(id: number) {
    this.userIds.update(ids => ids.filter(existingId => existingId !== id));
  }
}

Conditional Queries

@Component({})
export class ConditionalQueriesComponent {
  #http = inject(HttpClient);
  
  showAdvanced = signal(false);
  userId = signal(1);
  
  // Conditionally include queries
  queries = computed(() => {
    const baseQueries = [
      {
        queryKey: ['user', this.userId()] as const,
        queryFn: () => this.#http.get<User>(`/api/users/${this.userId()}`)
      },
      {
        queryKey: ['posts', this.userId()] as const,
        queryFn: () => this.#http.get<Post[]>(`/api/users/${this.userId()}/posts`)
      }
    ];
    
    if (this.showAdvanced()) {
      baseQueries.push(
        {
          queryKey: ['analytics', this.userId()] as const,
          queryFn: () => this.#http.get<Analytics>(`/api/users/${this.userId()}/analytics`)
        },
        {
          queryKey: ['permissions', this.userId()] as const,
          queryFn: () => this.#http.get<Permissions>(`/api/users/${this.userId()}/permissions`)
        }
      );
    }
    
    return baseQueries;
  });
  
  data = injectQueries({
    queries: this.queries,
    combine: (results) => ({
      isLoading: results.some(r => r.isPending),
      hasError: results.some(r => r.isError),
      user: results[0]?.data,
      posts: results[1]?.data,
      analytics: results[2]?.data,
      permissions: results[3]?.data
    })
  });
}

Error Handling and Retry Logic

@Component({})
export class RobustQueriesComponent {
  #http = inject(HttpClient);
  
  criticalQueries = signal([
    {
      queryKey: ['critical-data'] as const,
      queryFn: () => this.#http.get<CriticalData>('/api/critical'),
      retry: 3,
      retryDelay: (attemptIndex: number) => Math.min(1000 * 2 ** attemptIndex, 30000)
    },
    {
      queryKey: ['user-preferences'] as const,
      queryFn: () => this.#http.get<UserPreferences>('/api/user/preferences'),
      retry: 1
    }
  ]);
  
  appData = injectQueries({
    queries: this.criticalQueries,
    combine: (results) => {
      const [criticalResult, preferencesResult] = results;
      
      // Handle different error scenarios
      if (criticalResult.isError && criticalResult.failureCount >= 3) {
        return {
          status: 'critical-error',
          error: criticalResult.error,
          canRetry: false
        };
      }
      
      if (results.some(r => r.isLoading)) {
        return {
          status: 'loading',
          progress: results.filter(r => r.isSuccess).length / results.length
        };
      }
      
      return {
        status: 'success',
        criticalData: criticalResult.data,
        preferences: preferencesResult.data,
        hasPartialData: criticalResult.isSuccess || preferencesResult.isSuccess
      };
    }
  });
}

Performance Optimization

@Component({})
export class OptimizedQueriesComponent {
  #http = inject(HttpClient);
  
  // Memoized queries to prevent unnecessary recreations
  queries = computed(() => {
    return [
      {
        queryKey: ['expensive-computation'] as const,
        queryFn: () => this.#http.get('/api/expensive'),
        staleTime: 10 * 60 * 1000, // 10 minutes
        gcTime: 30 * 60 * 1000 // 30 minutes
      },
      {
        queryKey: ['realtime-data'] as const,
        queryFn: () => this.#http.get('/api/realtime'),
        staleTime: 0, // Always fresh
        refetchInterval: 5000 // 5 seconds
      }
    ];
  });
  
  optimizedData = injectQueries({
    queries: this.queries,
    combine: (results) => {
      // Only recalculate when necessary
      const memoizedResult = {
        timestamp: Date.now(),
        data: results.map(r => r.data),
        isStale: results.some(r => r.isStale)
      };
      
      return memoizedResult;
    }
  });
}

Install with Tessl CLI

npx tessl i tessl/npm-tanstack--angular-query-experimental

docs

index.md

infinite-queries.md

multi-query-operations.md

mutation-management.md

options-helpers.md

provider-setup.md

query-management.md

status-monitoring.md

tile.json