-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
Add onSuccess/onError/onSettled callbacks to useQuery #9943
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
WalkthroughAdds three new optional lifecycle callbacks— Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes
Suggested labels
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (14)
examples/react/nextjs-suspense-streaming/tsconfig.json (1)
4-8: Formatting improvements enhance readability.The multi-line reformatting of the
libandexcludearrays improves readability without affecting functionality. These changes follow common configuration conventions for better maintainability.Also applies to: 34-36
docs/framework/react/reference/useQuery.md (1)
46-48: ClarifyonSettledargument semantics to match the signatureThe new callbacks and their signatures look consistent with the core types, but the
onSettleddescription (“passed either thedataorerror”) is a bit ambiguous given the(data, error)signature. Consider tightening the copy to spell out that on successdatais defined anderrorisnull, and on failuredataisundefinedanderroris set, to avoid confusion and to mirror howuseMutationdocuments its callbacks.Also applies to: 112-123
docs/framework/solid/reference/useQuery.md (1)
46-48: KeeponSettleddocs precise and consistent with React variantThe added callbacks are documented consistently with React, but the
onSettledtext (“be passed either thedataorerror”) doesn’t quite match the(data, error)signature. I’d mirror whatever clarification you apply in the React docs here (explicitly noting which param is set on success vs. error) so Solid users get the same, precise contract.Also applies to: 250-261
packages/vue-query/src/__tests__/useQuery.test.ts (1)
574-601: Strengthen callback assertions for better regression coverageThis test currently only checks that the four callbacks were invoked. Since core tests already verify ordering, it would still be cheap here to assert that the success callbacks received
'fetched'and the error callbacks received anErrorinstance (and maybetoHaveBeenCalledTimes(1)), which would better protect the wrapper’s wiring without much extra noise.packages/react-query/src/__tests__/useQuery.test.tsx (2)
6471-6474: Simplify thequeryFnin the skipToken test for consistencyThe async
queryFnhere works, butawait sleep(10)followed byreturn Promise.resolve('data')is more complex than needed. For consistency with the rest of this file and prior learnings, consider the more concise pattern:queryFn: enabled ? () => sleep(10).then(() => 'data') : skipTokenwhich keeps the timing semantics while using the standard
.thenstyle for side‑effect‑free test queryFns.
Based on learnings, …
6778-6811: Consider asserting callback arguments in addition to invocationsThis test does a good job checking that
onSuccess,onError, andonSettledare all called for success and failure paths. To catch more subtle regressions, you might also assert that the success callbacks receive'fetched'and the failure callbacks receive anError(and possiblytoHaveBeenCalledTimes(1)), similar to the core tests.packages/query-core/src/__tests__/query.test.tsx (2)
1308-1329: Align test assertions with “correct order” wording in the descriptionThe implementation looks fine, but this test currently verifies that
onErrorandonSettledare each called once with the expected arguments, not that they are called in a particular order. Either add an explicit ordering check (e.g., viamock.invocationCallOrderor by recording a shared log) or relax the test name to avoid implying an order guarantee you’re not actually asserting.
1375-1402: Expand success/failure callback test to validate arguments and countsThis test confirms that all four callbacks are invoked, which is useful, but you could cheaply increase its value by asserting that:
onSuccessMockandonSuccessSettledMockwere called exactly once with'fetched', andonFailureMock/onFailureSettledMockwere called exactly once with anErrorinstance.That would better pin down the contract of the new lifecycle callbacks at the core level.
packages/angular-query-experimental/src/__tests__/inject-query.test.ts (1)
771-801: Consider verifying callback arguments for more robust assertions.The test verifies that callbacks are invoked but doesn't assert the arguments passed to them. The
queryObserver.test.tsxtests in this PR verify arguments liketoHaveBeenLastCalledWith('SUCCESS')andtoHaveBeenLastCalledWith('SUCCESS', null). Adding similar assertions here would improve test coverage and catch regressions in argument handling.Additionally, the test title mentions "after a successful fetch" but it tests both success and error cases. Consider rephrasing to "callbacks
onSuccess,onErrorandonSettledshould be called after fetch completes" for clarity.- test('callbacks `onSuccess`, `onError` and `onSettled` should be called after a successful fetch', async () => { + test('callbacks `onSuccess`, `onError` and `onSettled` should be called after fetch completes', async () => { const onSuccessMock = vi.fn() const onFailureMock = vi.fn() const onSuccessSettledMock = vi.fn() const onFailureSettledMock = vi.fn() - TestBed.runInInjectionContext(() => { injectQuery(() => ({ queryKey: ['expected-success'], queryFn: () => 'fetched', onSuccess: (data) => onSuccessMock(data), onSettled: (data, _) => onSuccessSettledMock(data), - })); + })) injectQuery(() => ({ queryKey: ['expected-failure'], queryFn: () => Promise.reject(new Error('error')), onError: (error) => onFailureMock(error), onSettled: (_, error) => onFailureSettledMock(error), retry: false, - })); + })) }) await vi.advanceTimersByTimeAsync(10) - expect(onSuccessMock).toHaveBeenCalled() - expect(onSuccessSettledMock).toHaveBeenCalled() - expect(onFailureMock).toHaveBeenCalled() - expect(onFailureSettledMock).toHaveBeenCalled() + expect(onSuccessMock).toHaveBeenCalledWith('fetched') + expect(onSuccessSettledMock).toHaveBeenCalledWith('fetched') + expect(onFailureMock).toHaveBeenCalledWith(expect.any(Error)) + expect(onFailureSettledMock).toHaveBeenCalledWith(expect.any(Error)) })packages/svelte-query/tests/createQuery.svelte.test.ts (1)
1921-1957: Consider adding argument assertions for consistency with other tests in this PR.Similar to the Angular test, this test verifies callbacks are invoked but doesn't assert the arguments. The
queryObserver.test.tsxtests verify specific arguments. Consider adding assertions like:await sleep(10) - expect(onSuccessMock).toHaveBeenCalled() - expect(onSuccessSettledMock).toHaveBeenCalled() - expect(onFailureMock).toHaveBeenCalled() - expect(onFailureSettledMock).toHaveBeenCalled() + expect(onSuccessMock).toHaveBeenCalledWith('fetched') + expect(onSuccessSettledMock).toHaveBeenCalledWith('fetched') + expect(onFailureMock).toHaveBeenCalledWith(expect.any(Error)) + expect(onFailureSettledMock).toHaveBeenCalledWith(expect.any(Error))packages/solid-query/src/__tests__/useQuery.test.tsx (1)
6171-6208: Strengthen lifecycle callback assertionsThe test currently only checks that the four callbacks were invoked. To better lock in behavior, consider also asserting the arguments:
onSuccessMock/ successonSettledreceive'fetched'.onFailureMock/ failureonSettledreceive anError('error').This will catch regressions where callbacks fire with wrong payloads or at the wrong time.
packages/query-core/src/queryObserver.ts (3)
153-163:enabledruntime guard is safe butresolveEnabledcall is redundantThe guard correctly rejects non‑boolean/non‑function
enabledvalues at runtime. However, because the condition only executes whenenabledis neither boolean nor function,resolveEnabledwill always just echo the invalid value and thetypeof … !== 'boolean'check will always be true. You can simplify this to a direct type check onthis.options.enabledwithout callingresolveEnabled, which also avoids relying onthis.#currentQuerybefore#updateQueryhas run.
199-217: Avoid repeatedresolveEnabled/resolveStaleTimecalls insetOptions
setOptionscurrently recomputesresolveEnabled(andresolveStaleTime) multiple times with the same arguments when deciding whether to update timers. This is correct but means running user‑providedenabled/staleTimefunctions more than necessary.You could cache the resolved values for
this.optionsandprevOptionsinto locals and reuse them in the conditionals to reduce repeated work and make the logic easier to follow, while keeping behavior identical.
505-509: Tighten typing forplaceholderDatacallback andpreviousQueryThe new call passes both previous data and
previousQueryintoplaceholderData, which is great, but the cast viaunknown as PlaceholderDataFunction<TQueryData>andthis.#lastQueryWithDefinedData as anyloses type information.You can likely reduce
anyusage and improve safety by casting to the full generic:const placeholderFn = options.placeholderData as PlaceholderDataFunction< TQueryFnData, TError, TQueryData, TQueryKey > placeholderData = placeholderFn( this.#lastQueryWithDefinedData?.state.data, this.#lastQueryWithDefinedData, )This keeps the runtime behavior the same while preserving the actual query generics.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (13)
docs/framework/react/reference/useQuery.md(2 hunks)docs/framework/solid/reference/useQuery.md(2 hunks)examples/react/nextjs-suspense-streaming/tsconfig.json(3 hunks)packages/angular-query-experimental/src/__tests__/inject-query.test.ts(1 hunks)packages/query-core/src/__tests__/query.test.tsx(2 hunks)packages/query-core/src/__tests__/queryObserver.test.tsx(1 hunks)packages/query-core/src/query.ts(2 hunks)packages/query-core/src/queryObserver.ts(8 hunks)packages/query-core/src/types.ts(1 hunks)packages/react-query/src/__tests__/useQuery.test.tsx(2 hunks)packages/solid-query/src/__tests__/useQuery.test.tsx(2 hunks)packages/svelte-query/tests/createQuery.svelte.test.ts(2 hunks)packages/vue-query/src/__tests__/useQuery.test.ts(1 hunks)
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: sukvvon
Repo: TanStack/query PR: 9892
File: packages/solid-query-persist-client/src/__tests__/PersistQueryClientProvider.test.tsx:331-335
Timestamp: 2025-11-22T09:06:05.219Z
Learning: In TanStack/query test files, when a queryFn contains side effects (e.g., setting flags for test verification), prefer async/await syntax for clarity; when there are no side effects, prefer the .then() pattern for conciseness.
📚 Learning: 2025-11-22T09:06:05.219Z
Learnt from: sukvvon
Repo: TanStack/query PR: 9892
File: packages/solid-query-persist-client/src/__tests__/PersistQueryClientProvider.test.tsx:331-335
Timestamp: 2025-11-22T09:06:05.219Z
Learning: In TanStack/query test files, when a queryFn contains side effects (e.g., setting flags for test verification), prefer async/await syntax for clarity; when there are no side effects, prefer the .then() pattern for conciseness.
Applied to files:
packages/vue-query/src/__tests__/useQuery.test.tspackages/query-core/src/__tests__/queryObserver.test.tsxpackages/query-core/src/__tests__/query.test.tsxpackages/react-query/src/__tests__/useQuery.test.tsxpackages/solid-query/src/__tests__/useQuery.test.tsxpackages/svelte-query/tests/createQuery.svelte.test.tspackages/angular-query-experimental/src/__tests__/inject-query.test.tspackages/query-core/src/query.ts
📚 Learning: 2025-11-02T22:52:33.071Z
Learnt from: DogPawHat
Repo: TanStack/query PR: 9835
File: packages/query-core/src/__tests__/queryClient.test-d.tsx:242-256
Timestamp: 2025-11-02T22:52:33.071Z
Learning: In the TanStack Query codebase, the new `query` and `infiniteQuery` methods support the `select` option for data transformation, while the legacy `fetchQuery` and `fetchInfiniteQuery` methods do not support `select` and should reject it at the type level.
Applied to files:
packages/query-core/src/types.tspackages/query-core/src/query.ts
🧬 Code graph analysis (6)
packages/vue-query/src/__tests__/useQuery.test.ts (1)
packages/vue-query/src/useQuery.ts (1)
useQuery(123-137)
packages/react-query/src/__tests__/useQuery.test.tsx (2)
packages/query-core/src/utils.ts (1)
sleep(374-378)packages/react-query/src/__tests__/utils.tsx (1)
renderWithClient(9-23)
packages/query-core/src/queryObserver.ts (3)
packages/query-core/src/utils.ts (2)
resolveEnabled(126-136)resolveStaleTime(112-124)packages/query-core/src/types.ts (1)
PlaceholderDataFunction(171-179)packages/query-core/src/notifyManager.ts (1)
notifyManager(99-99)
packages/solid-query/src/__tests__/useQuery.test.tsx (3)
packages/solid-query/src/index.ts (2)
useQuery(88-88)QueryClientProvider(98-98)packages/solid-query/src/useQuery.ts (1)
useQuery(36-50)packages/solid-query/src/QueryClientProvider.tsx (1)
QueryClientProvider(32-47)
packages/svelte-query/tests/createQuery.svelte.test.ts (1)
packages/svelte-query/tests/utils.svelte.ts (1)
withEffectRoot(24-33)
packages/angular-query-experimental/src/__tests__/inject-query.test.ts (2)
packages/angular-query-experimental/src/index.ts (1)
injectQuery(45-45)packages/angular-query-experimental/src/inject-query.ts (1)
injectQuery(218-226)
🔇 Additional comments (8)
examples/react/nextjs-suspense-streaming/tsconfig.json (1)
19-19: Upgrade to modern JSX transform aligns with React 19.The change from
"preserve"to"react-jsx"enables the modern JSX transform and is the recommended configuration for React 19+ projects. This eliminates the need to import React in every JSX file and improves developer experience.Please verify that your Next.js build and development server continue to work as expected with this configuration change. If you'd like, I can help verify compatibility by checking the React version in package.json and confirming this is the recommended setting for your setup.
packages/query-core/src/query.ts (2)
596-601: Error path callback implementation looks correct.The error handling path mirrors the success path appropriately, calling
onErrorthenonSettledwith the error. Theas anycast is consistent with the success path.
560-565: The review comment references code that does not exist in the specified file.The code snippet in the review shows
await this.options.onSuccess?.(data)at lines 560-565 ofpackages/query-core/src/query.ts. However, query.ts does not implement per-query callbacks (this.options.onSuccess,onError,onSettled). At the specified lines, the actual code only invokes cache-level callbacks (this.#cache.config.onSuccess), which are not awaited and therefore cannot propagate errors in the manner described.Per-query callbacks with the
awaitpattern exist only inpackages/query-core/src/mutation.ts. The concern about callback error propagation may apply to mutations, where errors in success/error paths are handled differently (success callbacks are not wrapped in try-catch, while error callbacks are).Likely an incorrect or invalid review comment.
packages/query-core/src/__tests__/queryObserver.test.tsx (2)
937-961: LGTM!The success case test properly verifies both callback invocation and the correct arguments (
'SUCCESS'for onSuccess,'SUCCESS', nullfor onSettled). The test follows the existing patterns in the file.
963-991: LGTM!The error case test correctly verifies that
onErrorreceives the error object andonSettledreceives(undefined, Error). The approach of checkingonErrorLast.messageis appropriate for validating the error structure.packages/solid-query/src/__tests__/useQuery.test.tsx (1)
913-948: Selector error test looks correct and targetedThis test nicely exercises the
selecterror path and verifies that the resulting observer state exposes the thrown error instance as expected. No issues from a correctness perspective.packages/query-core/src/types.ts (2)
171-179:PlaceholderDataFunctionextension matches new usageExposing
previousQueryalongsidepreviousDatainPlaceholderDataFunctionlooks consistent with howcreateResultnow calls placeholder functions. This should give advanced users more context when deriving placeholder values without breaking existing callbacks that only accept a single parameter.
260-269: Callbacks do not exist on query options in TanStack Query v5The premise of this review is outdated. In TanStack Query v5,
onSuccess,onError, andonSettledcallbacks were removed entirely from queries and now exist only on mutations. The code snippet shown (lines 260–269) containsstructuralSharingandmetaoptions, not callback definitions. There is no type hierarchy concern regardingTDatavsTQueryDatafor query callbacks because query callbacks no longer exist. Users should read the result data directly from the hook or query object instead of using callbacks.Likely an incorrect or invalid review comment.
| } | ||
|
|
||
| this.#currentResult = nextResult | ||
| this.#currentResult = nextResult |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lifecycle callbacks should be based on observer result, not raw query.state
Right now #notify decides which callbacks to fire and which arguments to pass by inspecting notifyOptions.query.state.status/data/error. This diverges from the observer’s computed result in several important cases:
-
select(and select errors):createResultcan setstatus = 'error'anderror = this.#selectErrorsolely at the observer level.query.state.statusandquery.state.errorremain whatever the fetch layer set ('success'/null).- With the current logic,
onError/erroronSettledwill never see select errors; instead, the success branch may fire with stale data.
-
placeholderData:- When placeholder data is injected,
createResultmarksstatus = 'success'andisPlaceholderData = true, butquery.state.statuscan still be'pending'. - The callbacks will not see this “successful” placeholder state at all.
- When placeholder data is injected,
-
Refetching after success/error:
- For background refetches of a successful query,
query.state.statusstays'success'while onlyfetchStatustoggles. - This means
onSuccess/onSettledwill fire both when refetch starts (no new data) and again when it completes, which is likely more often than intended.
- For background refetches of a successful query,
Because updateResult already computes nextResult and assigns this.#currentResult immediately before calling #notify, you can avoid these inconsistencies by basing the callbacks on this.#currentResult (or nextResult) instead:
- Use
this.#currentResult.statusto choose between success/error branches. - Pass
this.#currentResult.dataandthis.#currentResult.errorintoonSuccess/onError/onSettled. - Optionally gate firing on meaningful transitions (e.g. status or
dataUpdatedAtchange) to avoid duplicate callbacks when only fetchStatus flips.
This would align the lifecycle hooks with what hook consumers actually see in UseQueryResult, and ensure select/placeholder behaviors are reflected correctly.
Also applies to: 731-752
🤖 Prompt for AI Agents
In packages/query-core/src/queryObserver.ts around lines 665 (also applies to
731-752), the notify logic currently reads notifyOptions.query.state.* to decide
which lifecycle callbacks to call and what args to pass; change it to use the
observer’s computed result (this.#currentResult or nextResult) instead: switch
on this.#currentResult.status for success/error branches, pass
this.#currentResult.data and this.#currentResult.error into
onSuccess/onError/onSettled, and add simple guards to only fire callbacks on
meaningful transitions (e.g., status change or dataUpdatedAt change) to avoid
duplicate calls when only fetchStatus flips.
🎯 Changes
This PR introduces
onSuccess,onError, andonSettledcallbacks to theuseQueryhook, aligning its API withuseMutationfor consistency and improved developer experience.Motivation:
Consistency:
useMutationalready provides these callbacks, and adding them touseQuerycreates a unified API across TanStack Query.Convenience: Callbacks allow users to handle side effects (e.g., showing notifications, logging, or state updates) directly within the query definition, reducing boilerplate and avoiding manual
useEffectas workaround.Use Cases:
Prevent unnecessary useEffects like this one
you need to read the all
useEffectto know what it do, and when it will run.Also
isSuccesscan be false, so you need to check to be safe`Implementation Notes:
data,errorobjects).✅ Checklist
pnpm run test:pr.🚀 Release Impact
Summary by CodeRabbit
New Features
Documentation
Tests
✏️ Tip: You can customize this high-level summary in your review settings.