Skip to content

Commit 19445a0

Browse files
fix(query-core): infinite re-renders in useQueries (#9639)
* test(useQueries): add test for preventing infinite re-renders * fix(query-core): resolve infinite re-renders in queriesObserver * style(useQueries): remove unnecessary comments * ci: apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent ccedf33 commit 19445a0

File tree

2 files changed

+81
-10
lines changed

2 files changed

+81
-10
lines changed

packages/query-core/src/queriesObserver.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -118,29 +118,30 @@ export class QueriesObserver<
118118
observer.getCurrentResult(),
119119
)
120120

121+
const hasLengthChange = prevObservers.length !== newObservers.length
121122
const hasIndexChange = newObservers.some(
122123
(observer, index) => observer !== prevObservers[index],
123124
)
125+
const hasStructuralChange = hasLengthChange || hasIndexChange
124126

125-
const hasResultChange =
126-
prevObservers.length === newObservers.length && !hasIndexChange
127-
? newResult.some((result, index) => {
128-
const prev = this.#result[index]
129-
return !prev || !shallowEqualObjects(result, prev)
130-
})
131-
: true
127+
const hasResultChange = hasStructuralChange
128+
? true
129+
: newResult.some((result, index) => {
130+
const prev = this.#result[index]
131+
return !prev || !shallowEqualObjects(result, prev)
132+
})
132133

133-
if (!hasIndexChange && !hasResultChange) return
134+
if (!hasStructuralChange && !hasResultChange) return
134135

135-
if (hasIndexChange) {
136+
if (hasStructuralChange) {
136137
this.#observers = newObservers
137138
}
138139

139140
this.#result = newResult
140141

141142
if (!this.hasListeners()) return
142143

143-
if (hasIndexChange) {
144+
if (hasStructuralChange) {
144145
difference(prevObservers, newObservers).forEach((observer) => {
145146
observer.destroy()
146147
})

packages/react-query/src/__tests__/useQueries.test.tsx

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1741,4 +1741,74 @@ describe('useQueries', () => {
17411741
// still no extra calls to combine
17421742
expect(spy).toHaveBeenCalledTimes(3)
17431743
})
1744+
1745+
it('should not cause infinite re-renders when removing last query', async () => {
1746+
let renderCount = 0
1747+
1748+
function Page() {
1749+
const [queries, setQueries] = React.useState([
1750+
{
1751+
queryKey: ['query1'],
1752+
queryFn: () => 'data1',
1753+
},
1754+
{
1755+
queryKey: ['query2'],
1756+
queryFn: () => 'data2',
1757+
},
1758+
])
1759+
renderCount++
1760+
1761+
const result = useQueries({ queries })
1762+
1763+
return (
1764+
<div>
1765+
<div data-testid="render-count">renders: {renderCount}</div>
1766+
<div data-testid="query-count">queries: {result.length}</div>
1767+
<button
1768+
onClick={() => {
1769+
setQueries([
1770+
{
1771+
queryKey: ['query1'],
1772+
queryFn: () => 'data1',
1773+
},
1774+
])
1775+
}}
1776+
>
1777+
remove last
1778+
</button>
1779+
<button
1780+
onClick={() => {
1781+
setQueries([
1782+
{
1783+
queryKey: ['query2'],
1784+
queryFn: () => 'data2',
1785+
},
1786+
])
1787+
}}
1788+
>
1789+
remove first
1790+
</button>
1791+
</div>
1792+
)
1793+
}
1794+
1795+
const rendered = renderWithClient(queryClient, <Page />)
1796+
1797+
await vi.advanceTimersByTimeAsync(0)
1798+
renderCount = 0
1799+
1800+
fireEvent.click(rendered.getByRole('button', { name: /remove last/i }))
1801+
await vi.advanceTimersByTimeAsync(100)
1802+
1803+
expect(renderCount).toBeLessThan(10)
1804+
expect(rendered.getByTestId('query-count').textContent).toBe('queries: 1')
1805+
1806+
renderCount = 0
1807+
1808+
fireEvent.click(rendered.getByRole('button', { name: /remove first/i }))
1809+
await vi.advanceTimersByTimeAsync(100)
1810+
1811+
expect(renderCount).toBeLessThan(10)
1812+
expect(rendered.getByTestId('query-count').textContent).toBe('queries: 1')
1813+
})
17441814
})

0 commit comments

Comments
 (0)