Skip to content
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

feat: add support for React.use() #7988

Merged
merged 115 commits into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
115 commits
Select commit Hold shift + click to select a range
8cbd6a0
let’s do it again
KATT Sep 1, 2024
739d023
fix test group
KATT Sep 1, 2024
7fe1d33
maybe
KATT Sep 1, 2024
757aeab
mkay
KATT Sep 1, 2024
7c88325
cool
KATT Sep 1, 2024
c2a2cb5
rm console.logs
KATT Sep 1, 2024
d290fca
mkay
KATT Sep 1, 2024
da88d68
mkay
KATT Sep 1, 2024
5e2767b
fix(vue-query): invalidate queries immediately after calling `invalid…
Mini-ghost Sep 1, 2024
5ec8342
release: v5.53.2
tannerlinsley Sep 1, 2024
1cae4fe
docs(vue-query): update SSR guide for nuxt2 (#8001)
DamianOsipiuk Sep 1, 2024
dc2fad6
thenable
KATT Sep 2, 2024
c4a1f9e
mkay
KATT Sep 2, 2024
4530246
Merge remote-tracking branch 'origin/main' into discussions-7980-prom…
KATT Sep 2, 2024
7faa2fa
Update packages/react-query/src/__tests__/useQuery.test.tsx
KATT Sep 2, 2024
e6583df
mkay
KATT Sep 2, 2024
4270093
mkay
KATT Sep 2, 2024
9cdc99d
faster and more consistent
KATT Sep 2, 2024
e704e3d
mkay
KATT Sep 2, 2024
e5cf4ee
mkay
KATT Sep 2, 2024
a3ea13e
mkay
KATT Sep 2, 2024
306bb1f
mkay
KATT Sep 2, 2024
ef3926a
mkay
KATT Sep 2, 2024
8993e37
fix unhandled rejections
KATT Sep 2, 2024
df946eb
more
KATT Sep 2, 2024
091a1b4
more
KATT Sep 2, 2024
05fc6c6
mkay
KATT Sep 2, 2024
2ecd43f
fix more
KATT Sep 2, 2024
b6b2d45
fixy
KATT Sep 2, 2024
34bb74e
cool
KATT Sep 2, 2024
739b66a
Merge branch 'main' into discussions-7980-promises
KATT Sep 2, 2024
36d367a
Update packages/react-query/package.json
KATT Sep 2, 2024
d1184ba
fix: track data property if `promise` is tracked
TkDodo Sep 2, 2024
dc85450
Revert "fix: track data property if `promise` is tracked"
KATT Sep 2, 2024
0d32020
add test case that @tkdodo was concerned about
KATT Sep 2, 2024
26d9067
tweak
KATT Sep 2, 2024
e1c2394
mkay
KATT Sep 2, 2024
6435af3
add `useInfiniteQuery()` test
KATT Sep 2, 2024
d4a2822
consistent testing
KATT Sep 2, 2024
88db1b9
better test
KATT Sep 2, 2024
cb0fa35
rm comment
KATT Sep 2, 2024
608dc61
test resetting errror boundary
KATT Sep 2, 2024
608e476
better test
KATT Sep 2, 2024
e419146
cool
KATT Sep 2, 2024
c7781ee
cool
KATT Sep 2, 2024
8491a96
more test
KATT Sep 3, 2024
9d0d7fa
Merge branch 'main' into discussions-7980-promises
KATT Sep 3, 2024
97aec78
mv cleanup
KATT Sep 3, 2024
b09263c
mkay
KATT Sep 3, 2024
f7b0359
some more things
KATT Sep 3, 2024
6356ab4
add fixme
KATT Sep 3, 2024
9d3b8bc
fix types
KATT Sep 3, 2024
d9cf744
wat
KATT Sep 3, 2024
001377d
Merge remote-tracking branch 'origin/main' into discussions-7980-prom…
KATT Sep 6, 2024
17b4743
fixes
KATT Sep 6, 2024
55b6c19
revert
KATT Sep 6, 2024
4bfa39d
fix
KATT Sep 6, 2024
f27f30d
colocating doesn’t workkk
KATT Sep 6, 2024
79ec573
mkay
KATT Sep 6, 2024
05de0fb
mkay
KATT Sep 6, 2024
25a5957
might work
KATT Sep 6, 2024
543f337
more test
KATT Sep 6, 2024
9e2147d
cool
KATT Sep 6, 2024
648acc2
i don’t know hwat i’m doing
KATT Sep 6, 2024
23ae156
mocky
KATT Sep 6, 2024
8967bee
lint
KATT Sep 6, 2024
7c7ab77
space
KATT Sep 6, 2024
1b24dcb
rm log
KATT Sep 6, 2024
7ad4230
setIsServer
KATT Sep 6, 2024
f1248f2
mkay
KATT Sep 6, 2024
8461f2a
ffs
KATT Sep 6, 2024
0275b3d
remove unnecessary stufffff
KATT Sep 6, 2024
8a64488
tweak more
KATT Sep 6, 2024
27996a4
just naming and comments
KATT Sep 6, 2024
011e373
tweak
KATT Sep 6, 2024
e217605
fix: use fetchOptimistic util instead of observer.fetchOptimistic
TkDodo Sep 9, 2024
be6c1f0
refactor: make sure to only trigger fetching during render if we real…
TkDodo Sep 9, 2024
6f8b777
Merge branch 'main' into discussions-7980-promises
TkDodo Sep 9, 2024
9b4e38b
fix: move the `isNewCacheEntry` check before observer creation
TkDodo Sep 9, 2024
b8cb318
chore: avoid rect key warnings
TkDodo Sep 9, 2024
3b33b03
fix: add an `updateResult` for all observers to finalize currentThenable
TkDodo Sep 9, 2024
86b8b03
chore: logs during suspense errors
TkDodo Sep 9, 2024
9df70ac
fix: empty catch
TkDodo Sep 9, 2024
ac8754a
feature flag
KATT Sep 13, 2024
1b7f5d4
Merge branch 'main' into discussions-7980-promises
KATT Sep 13, 2024
1e4f719
add comment
KATT Sep 13, 2024
4ee2728
simplify
KATT Sep 13, 2024
2fa83df
omit from suspense
KATT Sep 13, 2024
9f281f4
feat flag
KATT Sep 13, 2024
540ddd5
more tests
TkDodo Sep 14, 2024
b573cc6
test: scope experimental_promise to useQuery().promise tests
TkDodo Sep 14, 2024
c617a08
refactor: rename to experimental_prefetchInRender
TkDodo Sep 18, 2024
ae93100
test: more tests
TkDodo Sep 18, 2024
138b126
test: more cancelation
TkDodo Sep 18, 2024
d3bd66c
fix cancellation
KATT Sep 18, 2024
7941921
make it work
KATT Sep 19, 2024
a47709a
tweak comment
KATT Sep 19, 2024
0df1dc3
Update packages/react-query/src/useBaseQuery.ts
KATT Sep 19, 2024
2cbe35a
simplify code a bit
KATT Sep 19, 2024
60f830b
Update packages/query-core/src/queryObserver.ts
KATT Sep 19, 2024
b70d48c
refactor: move experimental_prefetchInRender check until after the ea…
TkDodo Sep 20, 2024
849b1b6
fix: when cancelled, the promise should stay pending
TkDodo Sep 20, 2024
c75afaa
test: disabled case
TkDodo Sep 20, 2024
dee4546
chore: no idea what's going on
TkDodo Sep 20, 2024
a9148ba
refactor: delete unnecessary check
TkDodo Sep 20, 2024
f01a06b
revert refactor i did for cancellation when we wanted it to `throw`
KATT Sep 20, 2024
954d85b
Merge branch 'main' into discussions-7980-promises
KATT Sep 20, 2024
2ffec73
Merge branch 'main' into discussions-7980-promises
TkDodo Sep 24, 2024
2c10c9c
Merge branch 'main' into discussions-7980-promises
KATT Sep 27, 2024
402a9d3
add docs
KATT Sep 27, 2024
3a6e344
align
KATT Sep 27, 2024
1ef234c
tweak
KATT Sep 27, 2024
7ea970a
Update docs/reference/QueryClient.md
KATT Sep 27, 2024
9e05c95
Update docs/framework/react/reference/queryOptions.md
TkDodo Sep 28, 2024
cb0dfb4
Merge branch 'main' into discussions-7980-promises
TkDodo Oct 1, 2024
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
50 changes: 50 additions & 0 deletions docs/framework/react/guides/suspense.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ React Query can also be used with React's Suspense for Data Fetching API's. For
- [useSuspenseQuery](../../reference/useSuspenseQuery)
- [useSuspenseInfiniteQuery](../../reference/useSuspenseInfiniteQuery)
- [useSuspenseQueries](../../reference/useSuspenseQueries)
- Additionally, you can use the `useQuery().promise` and `React.use()` (Experimental)

When using suspense mode, `status` states and `error` objects are not needed and are then replaced by usage of the `React.Suspense` component (including the use of the `fallback` prop and React error boundaries for catching errors). Please read the [Resetting Error Boundaries](#resetting-error-boundaries) and look at the [Suspense Example](https://stackblitz.com/github/TanStack/query/tree/main/examples/react/suspense) for more information on how to set up suspense mode.

Expand Down Expand Up @@ -172,3 +173,52 @@ export function Providers(props: { children: React.ReactNode }) {
```

For more information, check out the [NextJs Suspense Streaming Example](../../examples/nextjs-suspense-streaming) and the [Advanced Rendering & Hydration](../advanced-ssr) guide.

## Using `useQuery().promise` and `React.use()` (Experimental)

> To enable this feature, you need to set the `experimental_prefetchInRender` option to `true` when creating your `QueryClient`

**Example code:**

```tsx
const queryClient = new QueryClient({
defaultOptions: {
queries: {
experimental_prefetchInRender: true,
},
},
})
```

**Usage:**

```tsx
import React from 'react'
import { useQuery } from '@tanstack/react-query'
import { fetchTodos, type Todo } from './api'

function TodoList({ query }: { query: UseQueryResult<Todo[]> }) {
const data = React.use(query.promise)

return (
<ul>
{data.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
)
}

export function App() {
const query = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })

return (
<>
<h1>Todos</h1>
<React.Suspense fallback={<div>Loading...</div>}>
<TodoList query={query} />
</React.Suspense>
</>
)
}
```
5 changes: 5 additions & 0 deletions docs/framework/react/reference/queryOptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,8 @@ You can generally pass everything to `queryOptions` that you can also pass to [`
- `queryKey: QueryKey`
- **Required**
- The query key to generate options for.
- `experimental_prefetchInRender?: boolean`
- Optional
- Defaults to `false`
- When set to `true`, queries will be prefetched during render, which can be useful for certain optimization scenarios
- Needs to be turned on for the experimental `useQuery().promise` functionality
5 changes: 5 additions & 0 deletions docs/framework/react/reference/useInfiniteQuery.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const {
hasPreviousPage,
isFetchingNextPage,
isFetchingPreviousPage,
promise,
...result
} = useInfiniteQuery({
queryKey,
Expand Down Expand Up @@ -85,5 +86,9 @@ The returned properties for `useInfiniteQuery` are identical to the [`useQuery`
- Is the same as `isFetching && !isPending && !isFetchingNextPage && !isFetchingPreviousPage`
- `isRefetchError: boolean`
- Will be `true` if the query failed while refetching a page.
- `promise: Promise<TData>`
- A stable promise that resolves to the query result.
- This can be used with `React.use()` to fetch data
- Requires the `experimental_prefetchInRender` feature flag to be enabled on the `QueryClient`.

Keep in mind that imperative fetch calls, such as `fetchNextPage`, may interfere with the default refetch behaviour, resulting in outdated data. Make sure to call these functions only in response to user actions, or add conditions like `hasNextPage && !isFetching`.
4 changes: 4 additions & 0 deletions docs/framework/react/reference/useQuery.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const {
isRefetching,
isStale,
isSuccess,
promise,
refetch,
status,
} = useQuery(
Expand Down Expand Up @@ -244,3 +245,6 @@ const {
- Defaults to `true`
- Per default, a currently running request will be cancelled before a new request is made
- When set to `false`, no refetch will be made if there is already a request running.
- `promise: Promise<TData>`
- A stable promise that will be resolved with the data of the query.
- Requires the `experimental_prefetchInRender` feature flag to be enabled on the `QueryClient`.
102 changes: 101 additions & 1 deletion packages/query-core/src/__tests__/queryObserver.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@ describe('queryObserver', () => {
let queryClient: QueryClient

beforeEach(() => {
queryClient = createQueryClient()
queryClient = createQueryClient({
defaultOptions: {
queries: {
experimental_prefetchInRender: true,
},
},
})
queryClient.mount()
})

Expand Down Expand Up @@ -1133,4 +1139,98 @@ describe('queryObserver', () => {

unsubscribe()
})

test('should return a promise that resolves when data is present', async () => {
const results: Array<QueryObserverResult> = []
const key = queryKey()
let count = 0
const observer = new QueryObserver(queryClient, {
queryKey: key,
queryFn: () => {
if (++count > 9) {
return Promise.resolve('data')
}
throw new Error('rejected')
},
retry: 10,
retryDelay: 0,
})
const unsubscribe = observer.subscribe(() => {
results.push(observer.getCurrentResult())
})

await waitFor(() => {
expect(results.at(-1)?.data).toBe('data')
})

const numberOfUniquePromises = new Set(
results.map((result) => result.promise),
).size
expect(numberOfUniquePromises).toBe(1)

unsubscribe()
})

test('should return a new promise after recovering from an error', async () => {
const results: Array<QueryObserverResult> = []
const key = queryKey()

let succeeds = false
let idx = 0
const observer = new QueryObserver(queryClient, {
queryKey: key,
queryFn: () => {
if (succeeds) {
return Promise.resolve('data')
}
throw new Error(`rejected #${++idx}`)
},
retry: 5,
retryDelay: 0,
})
const unsubscribe = observer.subscribe(() => {
results.push(observer.getCurrentResult())
})

await waitFor(() => {
expect(results.at(-1)?.status).toBe('error')
})

expect(
results.every((result) => result.promise === results[0]!.promise),
).toBe(true)

{
// fail again
const lengthBefore = results.length
observer.refetch()
await waitFor(() => {
expect(results.length).toBeGreaterThan(lengthBefore)
expect(results.at(-1)?.status).toBe('error')
})

const numberOfUniquePromises = new Set(
results.map((result) => result.promise),
).size

expect(numberOfUniquePromises).toBe(2)
}
{
// succeed
succeeds = true
observer.refetch()

await waitFor(() => {
results.at(-1)?.status === 'success'
})

const numberOfUniquePromises = new Set(
results.map((result) => result.promise),
).size

expect(numberOfUniquePromises).toBe(3)
}

unsubscribe()
})
})
67 changes: 63 additions & 4 deletions packages/query-core/src/queryObserver.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import { focusManager } from './focusManager'
import { notifyManager } from './notifyManager'
import { fetchState } from './query'
import { Subscribable } from './subscribable'
import { pendingThenable } from './thenable'
import {
isServer,
isValidTimeout,
Expand All @@ -8,12 +13,9 @@ import {
shallowEqualObjects,
timeUntilStale,
} from './utils'
import { notifyManager } from './notifyManager'
import { focusManager } from './focusManager'
import { Subscribable } from './subscribable'
import { fetchState } from './query'
import type { FetchOptions, Query, QueryState } from './query'
import type { QueryClient } from './queryClient'
import type { PendingThenable, Thenable } from './thenable'
import type {
DefaultError,
DefaultedQueryObserverOptions,
Expand Down Expand Up @@ -57,6 +59,7 @@ export class QueryObserver<
TQueryData,
TQueryKey
>
#currentThenable: Thenable<TData>
#selectError: TError | null
#selectFn?: (data: TQueryData) => TData
#selectResult?: TData
Expand All @@ -82,6 +85,13 @@ export class QueryObserver<

this.#client = client
this.#selectError = null
this.#currentThenable = pendingThenable()
TkDodo marked this conversation as resolved.
Show resolved Hide resolved
if (!this.options.experimental_prefetchInRender) {
this.#currentThenable.reject(
new Error('experimental_prefetchInRender feature flag is not enabled'),
)
}

this.bindMethods()
this.setOptions(options)
}
Expand Down Expand Up @@ -582,6 +592,7 @@ export class QueryObserver<
isRefetchError: isError && hasData,
isStale: isStale(query, options),
refetch: this.refetch,
promise: this.#currentThenable,
}

return result as QueryObserverResult<TData, TError>
Expand All @@ -593,6 +604,7 @@ export class QueryObserver<
| undefined

const nextResult = this.createResult(this.#currentQuery, this.options)

this.#currentResultState = this.#currentQuery.state
this.#currentResultOptions = this.options

Expand All @@ -605,6 +617,52 @@ export class QueryObserver<
return
}

if (this.options.experimental_prefetchInRender) {
const finalizeThenableIfPossible = (thenable: PendingThenable<TData>) => {
if (nextResult.status === 'error') {
thenable.reject(nextResult.error)
} else if (nextResult.data !== undefined) {
thenable.resolve(nextResult.data)
}
}

/**
* Create a new thenable and result promise when the results have changed
*/
const recreateThenable = () => {
const pending =
(this.#currentThenable =
nextResult.promise =
pendingThenable())

finalizeThenableIfPossible(pending)
}

const prevThenable = this.#currentThenable
switch (prevThenable.status) {
case 'pending':
// Finalize the previous thenable if it was pending
finalizeThenableIfPossible(prevThenable)
break
case 'fulfilled':
if (
nextResult.status === 'error' ||
nextResult.data !== prevThenable.value
) {
recreateThenable()
}
break
case 'rejected':
if (
nextResult.status !== 'error' ||
nextResult.error !== prevThenable.reason
) {
recreateThenable()
}
break
}
}

this.#currentResult = nextResult

// Determine which callbacks to trigger
Expand Down Expand Up @@ -639,6 +697,7 @@ export class QueryObserver<
return Object.keys(this.#currentResult).some((key) => {
const typedKey = key as keyof QueryObserverResult
const changed = this.#currentResult[typedKey] !== prevResult[typedKey]

return changed && includedProps.has(typedKey)
})
}
Expand Down
Loading
Loading