diff --git a/.changeset/calm-wolves-reconcile.md b/.changeset/calm-wolves-reconcile.md
new file mode 100644
index 00000000000..66e2122ffac
--- /dev/null
+++ b/.changeset/calm-wolves-reconcile.md
@@ -0,0 +1,5 @@
+---
+'@tanstack/solid-query': patch
+---
+
+Avoid calling custom reconciliation when query data is missing or unchanged.
diff --git a/packages/solid-query/src/__tests__/useQuery.test.tsx b/packages/solid-query/src/__tests__/useQuery.test.tsx
index f1ce24c4256..fe494f31785 100644
--- a/packages/solid-query/src/__tests__/useQuery.test.tsx
+++ b/packages/solid-query/src/__tests__/useQuery.test.tsx
@@ -944,6 +944,81 @@ describe('useQuery', () => {
return null
})
+ it('should only reconcile when observer data changes', async () => {
+ const key = queryKey()
+ const firstData = { count: 1 }
+ const secondData = { count: 2 }
+ const thirdData = { count: 3 }
+ const queryResults = [firstData, secondData, secondData, thirdData]
+ const reconciliationInputs: Array<[number | undefined, number]> = []
+ let fetchCount = 0
+ const reconcileData = vi.fn(
+ (oldData: { count: number } | undefined, newData: { count: number }) => {
+ reconciliationInputs.push([oldData?.count, newData.count])
+ return reconcile(newData)(oldData)
+ },
+ )
+
+ function Page() {
+ const state = useQuery(() => ({
+ queryKey: key,
+ queryFn: () => sleep(10).then(() => queryResults[fetchCount++]!),
+ reconcile: reconcileData,
+ }))
+
+ return (
+
+
+ data: {state.data?.count}
+ fetch status: {state.fetchStatus}
+
+ )
+ }
+
+ const rendered = renderWithClient(queryClient, () => )
+
+ expect(reconcileData).not.toHaveBeenCalled()
+
+ await vi.advanceTimersByTimeAsync(10)
+ expect(rendered.getByText('data: 1')).toBeInTheDocument()
+ expect(reconcileData).toHaveBeenCalledTimes(1)
+
+ fireEvent.click(rendered.getByRole('button', { name: /refetch/i }))
+ await vi.advanceTimersByTimeAsync(0)
+ expect(rendered.getByText('fetch status: fetching')).toBeInTheDocument()
+ expect(reconcileData).toHaveBeenCalledTimes(1)
+
+ await vi.advanceTimersByTimeAsync(10)
+ expect(rendered.getByText('data: 2')).toBeInTheDocument()
+ expect(rendered.getByText('fetch status: idle')).toBeInTheDocument()
+ expect(reconcileData).toHaveBeenCalledTimes(2)
+
+ fireEvent.click(rendered.getByRole('button', { name: /refetch/i }))
+ await vi.advanceTimersByTimeAsync(0)
+ expect(rendered.getByText('fetch status: fetching')).toBeInTheDocument()
+ expect(reconcileData).toHaveBeenCalledTimes(2)
+
+ await vi.advanceTimersByTimeAsync(10)
+ expect(rendered.getByText('data: 2')).toBeInTheDocument()
+ expect(rendered.getByText('fetch status: idle')).toBeInTheDocument()
+ expect(reconcileData).toHaveBeenCalledTimes(2)
+
+ fireEvent.click(rendered.getByRole('button', { name: /refetch/i }))
+ await vi.advanceTimersByTimeAsync(0)
+ expect(rendered.getByText('fetch status: fetching')).toBeInTheDocument()
+ expect(reconcileData).toHaveBeenCalledTimes(2)
+
+ await vi.advanceTimersByTimeAsync(10)
+ expect(rendered.getByText('data: 3')).toBeInTheDocument()
+ expect(rendered.getByText('fetch status: idle')).toBeInTheDocument()
+ expect(reconcileData).toHaveBeenCalledTimes(3)
+ expect(reconciliationInputs).toEqual([
+ [undefined, 1],
+ [1, 2],
+ [2, 3],
+ ])
+ })
+
it('should use query function from hook when the existing query does not have a query function', async () => {
const key = queryKey()
const results: Array> = []
diff --git a/packages/solid-query/src/useBaseQuery.ts b/packages/solid-query/src/useBaseQuery.ts
index 1cc45a420ba..2a7ff11e3f8 100644
--- a/packages/solid-query/src/useBaseQuery.ts
+++ b/packages/solid-query/src/useBaseQuery.ts
@@ -31,9 +31,15 @@ function reconcileFn(
| string
| false
| ((oldData: TData | undefined, newData: TData) => TData),
+ observerDataChanged: boolean,
queryHash?: string,
): QueryObserverResult {
- if (reconcileOption === false) return result
+ if (reconcileOption === false || result.data === undefined) {
+ return result
+ }
+ if (!observerDataChanged) {
+ return { ...result, data: store.data } as typeof result
+ }
if (typeof reconcileOption === 'function') {
const newData = reconcileOption(store.data, result.data as TData)
return { ...result, data: newData } as typeof result
@@ -143,6 +149,11 @@ export function useBaseQuery<
)
let observerResult = observer().getOptimisticResult(defaultedOptions())
+ // Reconciliation can retain the store's data reference after the observer
+ // moves to new data, so status-only updates must compare observer references.
+ let lastDataObserver = observer()
+ let lastDataQueryHash = lastDataObserver.getCurrentQuery().queryHash
+ let lastObserverData = observerResult.data
const [state, setState] =
createStore>(observerResult)
@@ -188,7 +199,13 @@ export function useBaseQuery<
}
function setStateWithReconciliation(res: typeof observerResult) {
- const opts = observer().options
+ const currentObserver = observer()
+ const opts = currentObserver.options
+ const queryHash = currentObserver.getCurrentQuery().queryHash
+ const observerDataChanged =
+ currentObserver !== lastDataObserver ||
+ queryHash !== lastDataQueryHash ||
+ res.data !== lastObserverData
// @ts-expect-error - Reconcile option is not correctly typed internally
const reconcileOptions = opts.reconcile
@@ -197,9 +214,13 @@ export function useBaseQuery<
store,
res,
reconcileOptions === undefined ? false : reconcileOptions,
- opts.queryHash,
+ observerDataChanged,
+ queryHash,
)
})
+ lastDataObserver = currentObserver
+ lastDataQueryHash = queryHash
+ lastObserverData = res.data
}
function createDeepSignal(): Signal {