Skip to content

Add Initial Data and Query Options Support to query-db-collection #346

@KyleAMathews

Description

@KyleAMathews

Background

TanStack Query provides several options for data initialization and transformation that are not currently exposed in query-db-collection. While TanStack DB has its own patterns (like live queries with .select()), some Query options would enhance the developer experience and provide feature parity.

Current State

  • initialData: Only supported in localOnlyCollectionOptions, not in queryCollectionOptions
  • placeholderData: Not supported
  • structuralSharing: Hard-coded to true in query.ts:320
  • refetchOnWindowFocus/refetchOnReconnect: Not configurable

Proposed Features

1. Initial Data Support

Add initialData option to QueryCollectionConfig for pre-populating collections:

export interface QueryCollectionConfig<TItem, TError, TQueryKey> {
  // ... existing options ...
  
  /**
   * Initial data to populate the collection before the first fetch
   * Useful for SSR hydration or cached data from previous sessions
   */
  initialData?: Array<TItem> | (() => Array<TItem>)
  
  /**
   * When the initial data was last updated (for staleness calculations)
   */
  initialDataUpdatedAt?: number | (() => number)
}

Implementation approach:

  • Apply initial data before starting the query observer
  • Mark as synced data so it appears immediately in live queries
  • Let TanStack Query's staleness logic determine if refetch is needed

2. Structural Sharing Configuration

Allow customizing how data updates are merged:

export interface QueryCollectionConfig<TItem, TError, TQueryKey> {
  // ... existing options ...
  
  /**
   * How to structurally share data between updates
   * - true: Use TanStack Query's default structural sharing
   * - false: Always use new references
   * - function: Custom sharing logic
   */
  structuralSharing?: boolean | ((oldData: Array<TItem>, newData: Array<TItem>) => Array<TItem>)
}

3. Refetch Triggers

Expose window focus and reconnect options:

export interface QueryCollectionConfig<TItem, TError, TQueryKey> {
  // ... existing options ...
  
  /**
   * Refetch on window focus
   * @default true (when data is stale)
   */
  refetchOnWindowFocus?: boolean | ((query: Query) => boolean)
  
  /**
   * Refetch on reconnect
   * @default true (when data is stale)
   */
  refetchOnReconnect?: boolean | ((query: Query) => boolean)
  
  /**
   * Network mode for offline behavior
   * @default 'online'
   */
  networkMode?: 'online' | 'always' | 'offlineFirst'
}

Implementation Examples

Example 1: SSR Hydration

// Server-side: fetch and serialize initial data
const initialTodos = await fetchTodos()

// Client-side: create collection with initial data
const todoCollection = createCollection(
  queryCollectionOptions({
    queryKey: ['todos'],
    queryFn: fetchTodos,
    getKey: (item) => item.id,
    
    // Hydrate with server data
    initialData: initialTodos,
    initialDataUpdatedAt: Date.now() - 60000, // 1 minute old
    
    // Keep using server data if fresh enough
    staleTime: 5 * 60 * 1000, // 5 minutes
    
    queryClient,
  })
)

Example 2: Offline-First Configuration

const offlineCollection = createCollection(
  queryCollectionOptions({
    queryKey: ['offline-data'],
    queryFn: fetchData,
    getKey: (item) => item.id,
    
    // Load from localStorage if available
    initialData: () => {
      const cached = localStorage.getItem('offline-data')
      return cached ? JSON.parse(cached) : []
    },
    
    // Don't refetch on focus if we have local data
    refetchOnWindowFocus: false,
    refetchOnReconnect: true,
    
    // Work offline-first
    networkMode: 'offlineFirst',
    
    queryClient,
  })
)

Example 3: Custom Structural Sharing

const configCollection = createCollection(
  queryCollectionOptions({
    queryKey: ['config'],
    queryFn: fetchConfig,
    getKey: (item) => item.key,
    
    // Custom structural sharing to preserve references for unchanged config values
    structuralSharing: (oldData, newData) => {
      const result = [...newData]
      const newMap = new Map(newData.map(item => [item.key, item]))
      
      // Reuse references for unchanged items
      oldData.forEach((oldItem, index) => {
        const newItem = newMap.get(oldItem.key)
        if (newItem && deepEqual(oldItem, newItem)) {
          result[index] = oldItem // Keep old reference
        }
      })
      
      return result
    },
    
    queryClient,
  })
)

Design Decisions

Why Not placeholderData?

Placeholder data in TanStack Query shows temporary data while loading. In TanStack DB:

  • Collections can have initialData that persists
  • Live queries show loading states via isLoading/isReady
  • The local-first model means you often have real data to show

If needed, developers can implement placeholder UI at the component level.

Benefits

  1. SSR Support: Initial data enables server-side rendering hydration
  2. Offline Capability: Better control over offline behavior
  3. Performance: Custom structural sharing can optimize re-renders
  4. Feature Parity: Developers familiar with TanStack Query can use known patterns

Testing Requirements

  1. Test initial data is properly synced to collection
  2. Test staleness calculations with initialDataUpdatedAt
  3. Test refetch triggers respect configuration
  4. Test custom structural sharing functions
  5. Test network mode behavior
  6. Test that initial data doesn't interfere with query observer

Migration Guide

All changes are backwards compatible. Existing collections continue to work with defaults:

  • structuralSharing: true
  • refetchOnWindowFocus: true (when stale)
  • refetchOnReconnect: true (when stale)
  • No initial data

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