Skip to content

fix(query-core): ensure combine re-executes after cache restoration with memoized combine - #9592

Merged
TkDodo merged 13 commits into
TanStack:mainfrom
joseph0926:fix/memoized-combine-persist-restore
Sep 2, 2025
Merged

fix(query-core): ensure combine re-executes after cache restoration with memoized combine#9592
TkDodo merged 13 commits into
TanStack:mainfrom
joseph0926:fix/memoized-combine-persist-restore

Conversation

@joseph0926

@joseph0926 joseph0926 commented Aug 26, 2025

Copy link
Copy Markdown
Contributor

Fixes: #9586

Fix combine function not re-executing after cache restoration with memoized combine

When using useQueries with a memoized combine function (via useCallback) alongside PersistQueryClientProvider, the UI fails to update after page refresh even though cached data exists.

The Problem

We discovered this issue while working with React Query's persist functionality. After refreshing the page, the combine function would return stale pending state despite the cache containing valid data. The root cause was in QueriesObserver's setQueries method.

During our debugging process, we traced through the execution flow and found that when isRestoring changes from true to false, setQueries gets called but hits an early return condition. Since the same observer instances are reused and no index changes occur, the method returns early without updating the results or notifying listeners.

The issue becomes more critical with React 19's automatic memoization, where all functions will be memoized by default.

The Solution

We modified the early return logic in setQueries to check if the actual results have changed. When observers haven't changed but the data or pending state differs, we now update this.#result and call this.#notify() to trigger the combine function re-execution.

if (prevObservers.length === newObservers.length && !hasIndexChange) {
  const resultChanged = newResult.some((result, index) => {
    const prev = this.#result[index]
    return !prev || result.data !== prev.data || result.isPending !== prev.isPending
  })

  if (resultChanged) {
    this.#result = newResult
    this.#notify()
  }
  
  return
}

This ensures the UI updates correctly while maintaining the performance optimization of not recreating observers unnecessarily.

Testing

Added test case to verify combine function works correctly with memoization and persist. All existing tests pass without modification.

Summary by CodeRabbit

  • Bug Fixes

    • Ensure UI updates when individual query results change (per-item shallow comparisons), even if observer identity or count remains the same.
  • Tests

    • Added integration test validating persisted queries work with a memoized combine and render pending/data after hydration.
    • Added test ensuring observers notify when underlying results change during reconfiguration.
    • Updated test expectations to account for additional renders with a stable combine reference.

Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

useQueries doesn鈥檛 call combine when StoragePersister deserializes data

2 participants