Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 20 additions & 7 deletions packages/query-core/src/queriesObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export class QueriesObserver<
#client: QueryClient
#result!: Array<QueryObserverResult>
#queries: Array<QueryObserverOptions>
#options?: QueriesObserverOptions<TCombinedResult>
#observers: Array<QueryObserver>
#combinedResult?: TCombinedResult
#lastCombine?: CombineFn<TCombinedResult>
Expand All @@ -46,11 +47,12 @@ export class QueriesObserver<
constructor(
client: QueryClient,
queries: Array<QueryObserverOptions<any, any, any, any, any>>,
_options?: QueriesObserverOptions<TCombinedResult>,
options?: QueriesObserverOptions<TCombinedResult>,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it safe to rely on this? If options.combine change in the hook, will this be reflected?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes because we update it here:

this.#options = options

) {
super()

this.#client = client
this.#options = options
this.#queries = []
this.#observers = []
this.#result = []
Expand Down Expand Up @@ -83,10 +85,11 @@ export class QueriesObserver<

setQueries(
queries: Array<QueryObserverOptions>,
_options?: QueriesObserverOptions<TCombinedResult>,
options?: QueriesObserverOptions<TCombinedResult>,
notifyOptions?: NotifyOptions,
): void {
this.#queries = queries
this.#options = options

notifyManager.batch(() => {
const prevObservers = this.#observers
Expand Down Expand Up @@ -268,11 +271,21 @@ export class QueriesObserver<
}

#notify(): void {
notifyManager.batch(() => {
this.listeners.forEach((listener) => {
listener(this.#result)
})
})
if (this.hasListeners()) {
const previousResult = this.#combinedResult
const newResult = this.#combineResult(
this.#result,
this.#options?.combine,
)

if (previousResult !== newResult) {
notifyManager.batch(() => {
this.listeners.forEach((listener) => {
listener(this.#result)
})
})
}
}
}
}

Expand Down
117 changes: 117 additions & 0 deletions packages/react-query/src/__tests__/useQueries.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1431,4 +1431,121 @@ describe('useQueries', () => {
// state changed, re-run combine
expect(spy).toHaveBeenCalledTimes(4)
})

it('should not re-render if combine returns a stable reference', async () => {
const key1 = queryKey()
const key2 = queryKey()

const client = new QueryClient()

const queryFns: Array<string> = []
let renders = 0

function Page() {
const data = useQueries(
{
queries: [
{
queryKey: [key1],
queryFn: async () => {
await sleep(10)
queryFns.push('first result')
return 'first result'
},
},
{
queryKey: [key2],
queryFn: async () => {
await sleep(20)
queryFns.push('second result')
return 'second result'
},
},
],
combine: () => 'foo',
},
client,
)

renders++

return (
<div>
<div>data: {data}</div>
</div>
)
}

const rendered = render(<Page />)

await waitFor(() => rendered.getByText('data: foo'))

await waitFor(() =>
expect(queryFns).toEqual(['first result', 'second result']),
)

expect(renders).toBe(1)
})

it('should re-render once combine returns a different reference', async () => {
const key1 = queryKey()
const key2 = queryKey()
const key3 = queryKey()

const client = new QueryClient()

let renders = 0

function Page() {
const data = useQueries(
{
queries: [
{
queryKey: [key1],
queryFn: async () => {
await sleep(10)
return 'first result'
},
},
{
queryKey: [key2],
queryFn: async () => {
await sleep(15)
return 'second result'
},
},
{
queryKey: [key3],
queryFn: async () => {
await sleep(20)
return 'third result'
},
},
],
combine: (results) => {
const isPending = results.some((res) => res.isPending)

return isPending ? 'pending' : 'foo'
},
},
client,
)

renders++

return (
<div>
<div>data: {data}</div>
</div>
)
}

const rendered = render(<Page />)

await waitFor(() => rendered.getByText('data: pending'))
await waitFor(() => rendered.getByText('data: foo'))

// one with pending, one with foo
expect(renders).toBe(2)
})
})
Loading