Repository navigation
fix(query-core): keep select error result stable when select throws the same error #12539
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@tanstack/query-core': patch | ||
| --- | ||
|
|
||
| Keep the observer result stable when `select` throws the same error again, so an inline selector no longer forces a render loop. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -73,6 +73,7 @@ export class QueryObserver< | |
| TQueryKey | ||
| > | ||
| #selectError: TError | null | ||
| #selectErrorUpdatedAt = 0 | ||
| #selectFn?: (data: TQueryData) => TData | ||
| #selectResult?: TData | ||
| // This property keeps track of the last query with defined data. | ||
|
|
@@ -697,20 +698,27 @@ export class QueryObserver< | |
| data = replaceData(prevResult?.data, data, options) | ||
| this.#selectResult = data | ||
| this.#selectError = null | ||
| this.#selectErrorUpdatedAt = 0 | ||
| } catch (selectError) { | ||
| this.#selectError = selectError as TError | ||
| // A new inline select throws a new object on every render. Replacing | ||
| // the stored error refreshes errorUpdatedAt and notifies subscribers. | ||
| if (selectErrorChanged(this.#selectError, selectError)) { | ||
| this.#selectError = selectError as TError | ||
| this.#selectErrorUpdatedAt = Date.now() | ||
| } | ||
| } | ||
| } | ||
| } else if (data === undefined) { | ||
| // a stored select error belongs to previously selected data; once that | ||
| // data is gone (query switch or reset), it must not leak into this result | ||
| this.#selectError = null | ||
| this.#selectErrorUpdatedAt = 0 | ||
| } | ||
|
|
||
| if (this.#selectError) { | ||
| error = this.#selectError | ||
| data = this.#selectResult | ||
| errorUpdatedAt = Date.now() | ||
| errorUpdatedAt = this.#selectErrorUpdatedAt | ||
| status = 'error' | ||
| isPlaceholderData = false | ||
| } | ||
|
|
@@ -974,3 +982,34 @@ function isStale( | |
| query.isStaleByTime(resolveQueryValue(options.staleTime, query)) | ||
| ) | ||
| } | ||
|
|
||
| /** | ||
| * Reports whether a value just thrown by `select` should replace the stored select error. | ||
| * A new `Error` with the same name, message, cause, and own fields is the same failure. | ||
| * That keeps `errorUpdatedAt` stable when an inline selector throws again on the next render. | ||
| * @param previous - The select error already stored, if there is one. | ||
| * @param next - The value just thrown by `select`. | ||
| * @returns `true` if `next` should replace `previous`. | ||
| */ | ||
| function selectErrorChanged(previous: unknown, next: unknown): boolean { | ||
| if (Object.is(previous, next)) { | ||
| return false | ||
| } | ||
|
|
||
| if (!(previous instanceof Error) || !(next instanceof Error)) { | ||
| return true | ||
| } | ||
|
|
||
| if ( | ||
| previous.name !== next.name || | ||
| previous.message !== next.message || | ||
| !Object.is(previous.cause, next.cause) | ||
| ) { | ||
| return true | ||
| } | ||
|
|
||
| return !shallowEqualObjects( | ||
| previous as unknown as Record<string, any>, | ||
| next as unknown as Record<string, any>, | ||
| ) | ||
| } | ||
|
Comment on lines
+994
to
+1015
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
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.
Script executed:
Repository: TanStack/query
Length of output: 25421
Script executed:
Repository: TanStack/query
Length of output: 23444
Preserve changed error details when deduplicating selector failures.
If successive errors have different non-enumerable
codevalues, the observer retains the first error and timestamp. Consumers see the old code and receive no notification for the new failure. Preserve meaningful error details without reintroducing the inline-selector render loop.