Skip to content

Document QueryFunctionContext signal and cancellation semantics #350

Description

@KyleAMathews

Status — narrowed 2026-07-21

QueryFunctionContext.meta and QueryFunctionContext.signal are already exposed to queryFn:

  • custom meta is merged with adapter-owned loadSubsetOptions;
  • Query Core's AbortSignal reaches queryFn unchanged;
  • explicit collection cleanup aborts an in-flight request when the query function consumes the signal;
  • late success and rejection after subset cleanup cannot rematerialize rows or leak readiness listeners.

The remaining task is documentation:

  1. Show passing ctx.signal to fetch or another abortable client.
  2. State that explicit collection cleanup aborts cancellable requests.
  3. State that unloading the last on-demand subset does not currently cancel an in-flight request, preserving Query cache reuse and fast remount behavior.
  4. Link broader cancellation and lease-policy work to RFC #1657 and open PR #1573, whose current cancellation patch is not merge-ready.

No new runtime API is required for this issue.


Original proposal

Background

TanStack Query provides a QueryFunctionContext to query functions with useful properties like signal for cancellation and meta for passing additional context. Currently, query-db-collection passes this context to the queryFn but doesn't fully utilize or expose these capabilities.

Current State

// TanStack Query provides:
interface QueryFunctionContext {
  queryKey: QueryKey
  signal?: AbortSignal  // For cancellation
  meta?: Record<string, unknown>  // Metadata
  pageParam?: unknown  // For infinite queries
}

// Current usage in query-db-collection:
queryFn: async (context) => {
  // context is passed but signal and meta aren't commonly used
  return api.fetchTodos()
}

Problems

  1. No request cancellation: Can't cancel in-flight requests when collections are cleaned up
  2. No metadata support: Can't pass additional context through the query system
  3. Missed optimization: Unnecessary network requests continue even after cleanup

Proposed Solution

1. Document and encourage signal usage

const todoCollection = createCollection(
  queryCollectionOptions({
    queryKey: ['todos'],
    queryFn: async (context) => {
      // Use signal for cancellable requests
      const response = await fetch('/api/todos', {
        signal: context.signal
      })
      
      if (\!response.ok) throw new Error('Failed to fetch')
      return response.json()
    },
    getKey: (item) => item.id,
    queryClient,
  })
)

2. Add meta support to QueryCollectionConfig

export interface QueryCollectionConfig<TItem, TError, TQueryKey> {
  // ... existing options ...
  
  /**
   * Metadata to pass to the query
   * Available in queryFn via context.meta
   */
  meta?: Record<string, unknown>
}

Implementation Examples

Example 1: Cancellable Fetch with AbortSignal

const searchCollection = createCollection(
  queryCollectionOptions({
    queryKey: ['search', searchTerm],
    queryFn: async (context) => {
      try {
        const response = await fetch(`/api/search?q=${searchTerm}`, {
          signal: context.signal
        })
        return response.json()
      } catch (error) {
        if (error.name === 'AbortError') {
          console.log('Search cancelled')
          return [] // Return empty results for cancelled requests
        }
        throw error
      }
    },
    getKey: (item) => item.id,
    queryClient,
  })
)

Example 2: Using Meta for Error Context

const userCollection = createCollection(
  queryCollectionOptions({
    queryKey: ['users', departmentId],
    queryFn: async (context) => {
      try {
        return await api.getUsers(departmentId)
      } catch (error) {
        // Use meta for better error messages
        throw new Error(
          context.meta?.errorMessage || 'Failed to load users'
        )
      }
    },
    meta: {
      errorMessage: `Failed to load users for department ${departmentId}`,
      feature: 'user-management',
      retryable: true
    },
    getKey: (item) => item.id,
    queryClient,
  })
)

Example 3: GraphQL with Cancellation

const graphqlCollection = createCollection(
  queryCollectionOptions({
    queryKey: ['graphql-data'],
    queryFn: async (context) => {
      // GraphQL clients often support AbortSignal
      return graphqlClient.request(
        QUERY,
        variables,
        { signal: context.signal }
      )
    },
    meta: {
      queryName: 'GetDashboardData',
      version: 'v2'
    },
    getKey: (item) => item.id,
    queryClient,
  })
)

Technical Implementation

  1. Pass meta through observer options:
const observerOptions: QueryObserverOptions = {
  queryKey,
  queryFn,
  meta: config.meta, // Add this
  // ... other options
}
  1. Cancel query on cleanup:
return async () => {
  actualUnsubscribeFn()
  await queryClient.cancelQueries({ queryKey }) // This triggers signal.abort()
  queryClient.removeQueries({ queryKey })
}

Benefits

  1. Performance: Cancel unnecessary requests when navigating away
  2. Better debugging: Meta information helps trace queries
  3. Resource efficiency: Prevent orphaned requests from consuming bandwidth
  4. Feature parity: Match TanStack Query's full capabilities

Testing Requirements

  1. Test that signal.abort() is triggered on collection cleanup
  2. Test that cancelled requests don't update collection state
  3. Test that meta is passed through to queryFn
  4. Test that meta is available in error handlers
  5. Test compatibility with various HTTP clients (fetch, axios, etc.)

Documentation

Update examples to show proper signal usage and meta patterns, especially for:

  • Fetch API
  • Axios
  • GraphQL clients
  • Custom API wrappers

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions