Skip to content

Commit

Permalink
fix(react-query): make sure options are up-to-date when a mounted com…
Browse files Browse the repository at this point in the history
…ponent re-suspends
  • Loading branch information
TkDodo committed Nov 24, 2023
1 parent a46c8a7 commit fcc0b28
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 0 deletions.
98 changes: 98 additions & 0 deletions packages/react-query/src/__tests__/suspense.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -935,4 +935,102 @@ describe('useSuspenseQueries', () => {
// query should resume
await waitFor(() => rendered.getByText('Data 1'))
})

it('should throw error when queryKey changes and new query fails', async () => {
const consoleMock = vi
.spyOn(console, 'error')
.mockImplementation(() => undefined)
const key = queryKey()

function Page() {
const [fail, setFail] = React.useState(false)
const { data } = useSuspenseQuery({
queryKey: [key, fail],
queryFn: async () => {
await sleep(10)

if (fail) {
throw new Error('Suspense Error Bingo')
} else {
return 'data'
}
},
retry: 0,
})

return (
<div>
<button onClick={() => setFail(true)}>trigger fail</button>

<div>rendered: {String(data)}</div>
</div>
)
}

const rendered = renderWithClient(
queryClient,
<ErrorBoundary fallbackRender={() => <div>error boundary</div>}>
<React.Suspense fallback={'Loading...'}>
<Page />
</React.Suspense>
</ErrorBoundary>,
)

await waitFor(() => rendered.getByText('Loading...'))

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

fireEvent.click(rendered.getByText('trigger fail'))

await waitFor(() => rendered.getByText('error boundary'))

expect(consoleMock).toHaveBeenCalledWith(
expect.objectContaining(new Error('Suspense Error Bingo')),
)

consoleMock.mockRestore()
})

it('should keep previous data when wrapped in a transition', async () => {
const key = queryKey()

function Page() {
const [count, setCount] = React.useState(0)
const [isPending, startTransition] = React.useTransition()
const { data } = useSuspenseQuery({
queryKey: [key, count],
queryFn: async () => {
await sleep(10)
return 'data' + count
},
})

return (
<div>
<button onClick={() => startTransition(() => setCount(count + 1))}>
inc
</button>

<div>{isPending ? 'Pending...' : String(data)}</div>
</div>
)
}

const rendered = renderWithClient(
queryClient,
<React.Suspense fallback={'Loading...'}>
<Page />
</React.Suspense>,
)

await waitFor(() => rendered.getByText('Loading...'))

await waitFor(() => rendered.getByText('data0'))

fireEvent.click(rendered.getByText('inc'))

await waitFor(() => rendered.getByText('Pending...'))

await waitFor(() => rendered.getByText('data1'))
})
})
4 changes: 4 additions & 0 deletions packages/react-query/src/useBaseQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ export function useBaseQuery<

// Handle suspense
if (shouldSuspend(defaultedOptions, result)) {
// Do the same thing as the effect right above because the effect won't run
// when we suspend but also, the component won't re-mount so our observer would
// be out of date.
observer.setOptions(defaultedOptions, { listeners: false })
throw fetchOptimistic(defaultedOptions, observer, errorResetBoundary)
}

Expand Down
7 changes: 7 additions & 0 deletions packages/react-query/src/useQueries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,13 @@ export function useQueries<
: []

if (suspensePromises.length > 0) {
observer.setQueries(
defaultedQueries,
options as QueriesObserverOptions<TCombinedResult>,
{
listeners: false,
},
)
throw Promise.all(suspensePromises)
}
const observerQueries = observer.getQueries()
Expand Down

0 comments on commit fcc0b28

Please sign in to comment.