diff --git a/docs/framework/preact/reference/functions/infiniteQueryOptions.md b/docs/framework/preact/reference/functions/infiniteQueryOptions.md index d28fe91935..206087481c 100644 --- a/docs/framework/preact/reference/functions/infiniteQueryOptions.md +++ b/docs/framework/preact/reference/functions/infiniteQueryOptions.md @@ -9,7 +9,7 @@ title: infiniteQueryOptions function infiniteQueryOptions(options): UseInfiniteQueryOptions & object & QueryKeyWithDataTag, TError>; ``` -Defined in: [preact-query/src/infiniteQueryOptions.ts:157](https://github.com/TanStack/query/blob/main/packages/preact-query/src/infiniteQueryOptions.ts#L157) +Defined in: [preact-query/src/infiniteQueryOptions.ts:169](https://github.com/TanStack/query/blob/main/packages/preact-query/src/infiniteQueryOptions.ts#L169) You can generally pass everything to `infiniteQueryOptions` that you can also pass to `useInfiniteQuery`. These options can be shared across hooks and imperative APIs such as `queryClient.infiniteQuery`. @@ -55,6 +55,11 @@ The same options object, typed so that `queryKey` carries the inferred data type [useInfiniteQuery](useInfiniteQuery.md) to run an infinite query with these options. +### Remarks + +See [useInfiniteQuery](useInfiniteQuery.md) for examples that fetch further pages, from a button click or +automatically as the user scrolls. + ### Example ```tsx @@ -69,8 +74,18 @@ export const projectsOptions = infiniteQueryOptions({ }) function Projects() { - const { data } = useInfiniteQuery(projectsOptions) - return <>{data.pages.map((page) => page.projects.map((p) =>

{p.name}

))} + // `data` is never `undefined`, thanks to `initialData` — even if a refetch fails, so the + // list stays visible alongside the error. + const { data, isError, error } = useInfiniteQuery(projectsOptions) + + return ( +
+ {isError ? Error: {error.message} : null} +
    + {data.pages.map((page) => page.projects.map((p) =>
  • {p.name}
  • ))} +
+
+ ) } ``` @@ -80,7 +95,7 @@ function Projects() { function infiniteQueryOptions(options): OmitKeyof, "queryFn"> & object & QueryKeyWithDataTag, TError>; ``` -Defined in: [preact-query/src/infiniteQueryOptions.ts:233](https://github.com/TanStack/query/blob/main/packages/preact-query/src/infiniteQueryOptions.ts#L233) +Defined in: [preact-query/src/infiniteQueryOptions.ts:240](https://github.com/TanStack/query/blob/main/packages/preact-query/src/infiniteQueryOptions.ts#L240) You can generally pass everything to `infiniteQueryOptions` that you can also pass to `useInfiniteQuery`. These options can be shared across hooks and imperative APIs such as `queryClient.infiniteQuery`. @@ -120,18 +135,12 @@ The [UnusedSkipTokenInfiniteOptions](../type-aliases/UnusedSkipTokenInfiniteOpti The same options object, typed so that `queryKey` carries the inferred data type. -### Examples +### Remarks -```tsx -import { infiniteQueryOptions } from '@tanstack/preact-query' +See [useInfiniteQuery](useInfiniteQuery.md) for examples that fetch further pages, from a button click or +automatically as the user scrolls. -export const projectsOptions = infiniteQueryOptions({ - queryKey: ['projects'], - queryFn: ({ pageParam }) => fetchProjects(pageParam), - initialPageParam: 0, - getNextPageParam: (lastPage) => lastPage.nextId, -}) -``` +### Example A parameterized factory, reused across a hook and an imperative call with the same cache entry: ```tsx @@ -150,16 +159,21 @@ export const commentsOptions = (postId: string) => }) function Comments({ postId }: { postId: string }) { - const result = useInfiniteQuery(commentsOptions(postId)) - if (!result.isSuccess) return 'Loading...' + const { data, isPending, isError, error } = useInfiniteQuery(commentsOptions(postId)) + + if (isPending) return 'Loading...' + if (isError) return Error: {error.message} + return ( - <> - {result.data.pages.map((page) => page.comments.map((c) =>

{c.text}

))} - +
    + {data.pages.map((page) => page.comments.map((c) =>
  • {c.text}
  • ))} +
) } -// Elsewhere, e.g. to warm the cache before rendering ``: +// `commentsOptions` also works with imperative APIs like `queryClient.infiniteQuery` — +// see `useInfiniteQuery` for an example that warms the cache this way before rendering ``. +const postId = '1' queryClient.infiniteQuery(commentsOptions(postId)).catch(noop) ``` @@ -173,7 +187,7 @@ queryClient.infiniteQuery(commentsOptions(postId)).catch(noop) function infiniteQueryOptions(options): UseInfiniteQueryOptions & object & QueryKeyWithDataTag, TError>; ``` -Defined in: [preact-query/src/infiniteQueryOptions.ts:309](https://github.com/TanStack/query/blob/main/packages/preact-query/src/infiniteQueryOptions.ts#L309) +Defined in: [preact-query/src/infiniteQueryOptions.ts:311](https://github.com/TanStack/query/blob/main/packages/preact-query/src/infiniteQueryOptions.ts#L311) You can generally pass everything to `infiniteQueryOptions` that you can also pass to `useInfiniteQuery`. These options can be shared across hooks and imperative APIs such as `queryClient.infiniteQuery`. @@ -213,18 +227,12 @@ The [UndefinedInitialDataInfiniteOptions](../type-aliases/UndefinedInitialDataIn The same options object, typed so that `queryKey` carries the inferred data type. -### Examples +### Remarks -```tsx -import { infiniteQueryOptions } from '@tanstack/preact-query' +See [useInfiniteQuery](useInfiniteQuery.md) for examples that fetch further pages (from a button click or +automatically as the user scrolls) and that use `skipToken` to disable the query until `postId` is set. -export const projectsOptions = infiniteQueryOptions({ - queryKey: ['projects'], - queryFn: ({ pageParam }) => fetchProjects(pageParam), - initialPageParam: 0, - getNextPageParam: (lastPage) => lastPage.nextId, -}) -``` +### Example A parameterized factory, reused across a hook and an imperative call with the same cache entry: ```tsx @@ -243,16 +251,21 @@ export const commentsOptions = (postId: string) => }) function Comments({ postId }: { postId: string }) { - const result = useInfiniteQuery(commentsOptions(postId)) - if (!result.isSuccess) return 'Loading...' + const { data, isPending, isError, error } = useInfiniteQuery(commentsOptions(postId)) + + if (isPending) return 'Loading...' + if (isError) return Error: {error.message} + return ( - <> - {result.data.pages.map((page) => page.comments.map((c) =>

{c.text}

))} - +
    + {data.pages.map((page) => page.comments.map((c) =>
  • {c.text}
  • ))} +
) } -// Elsewhere, e.g. to warm the cache before rendering ``: +// `commentsOptions` also works with imperative APIs like `queryClient.infiniteQuery` — +// see `useInfiniteQuery` for an example that warms the cache this way before rendering ``. +const postId = '1' queryClient.infiniteQuery(commentsOptions(postId)).catch(noop) ``` diff --git a/docs/framework/preact/reference/functions/mutationOptions.md b/docs/framework/preact/reference/functions/mutationOptions.md index 8d2e39f62a..018495b962 100644 --- a/docs/framework/preact/reference/functions/mutationOptions.md +++ b/docs/framework/preact/reference/functions/mutationOptions.md @@ -9,7 +9,7 @@ title: mutationOptions function mutationOptions(options): WithRequired, "mutationKey">; ``` -Defined in: [preact-query/src/mutationOptions.ts:49](https://github.com/TanStack/query/blob/main/packages/preact-query/src/mutationOptions.ts#L49) +Defined in: [preact-query/src/mutationOptions.ts:34](https://github.com/TanStack/query/blob/main/packages/preact-query/src/mutationOptions.ts#L34) You can generally pass everything to `mutationOptions` that you can also pass to `useMutation`. A `mutationKey` is required on this overload so the mutation can be looked up later, e.g. with @@ -52,21 +52,7 @@ The same options object, unchanged. [useMutation](useMutation.md) to run the mutation these options describe. -### Examples - -```tsx -import { mutationOptions, useMutation } from '@tanstack/preact-query' - -export const createPostOptions = mutationOptions({ - mutationKey: ['posts', 'create'], - mutationFn: createPost, -}) - -function CreatePost() { - const mutation = useMutation(createPostOptions) - return -} -``` +### Example Looking the mutation up elsewhere via its `mutationKey`, e.g. for a global "saving…" indicator: ```tsx @@ -92,7 +78,7 @@ function SavingIndicator() { function mutationOptions(options): Omit, "mutationKey">; ``` -Defined in: [preact-query/src/mutationOptions.ts:87](https://github.com/TanStack/query/blob/main/packages/preact-query/src/mutationOptions.ts#L87) +Defined in: [preact-query/src/mutationOptions.ts:74](https://github.com/TanStack/query/blob/main/packages/preact-query/src/mutationOptions.ts#L74) You can generally pass everything to `mutationOptions` that you can also pass to `useMutation`. No `mutationKey` is required on this overload — use this when you don't need to look the mutation up later @@ -135,6 +121,11 @@ The same options object, unchanged. [useMutation](useMutation.md) to run the mutation these options describe. +### Remarks + +Without a `mutationKey`, the mutation can't be looked up elsewhere via `useMutationState` — see +the other overload's example for that. + ### Example ```tsx diff --git a/docs/framework/preact/reference/functions/queryOptions.md b/docs/framework/preact/reference/functions/queryOptions.md index 88e114030c..4e6f33a105 100644 --- a/docs/framework/preact/reference/functions/queryOptions.md +++ b/docs/framework/preact/reference/functions/queryOptions.md @@ -9,7 +9,7 @@ title: queryOptions function queryOptions(options): Omit, "queryFn"> & object & QueryKeyWithDataTag; ``` -Defined in: [preact-query/src/queryOptions.ts:131](https://github.com/TanStack/query/blob/main/packages/preact-query/src/queryOptions.ts#L131) +Defined in: [preact-query/src/queryOptions.ts:140](https://github.com/TanStack/query/blob/main/packages/preact-query/src/queryOptions.ts#L140) You can generally pass everything to `queryOptions` that you can also pass to `useQuery`. These options can be shared across hooks and imperative APIs such as `queryClient.query`. `options.queryKey` is required and @@ -63,9 +63,18 @@ export const postsOptions = queryOptions({ }) function Posts() { - // `data` is `Post[]`, never `undefined`, thanks to `initialData`. - const { data } = useQuery(postsOptions) - return <>{data.map((post) =>

{post.title}

)} + // `data` is `Post[]`, never `undefined`, thanks to `initialData` — even if a refetch fails, + // so the list stays visible alongside the error. + const { data, isError, error } = useQuery(postsOptions) + + return ( +
+ {isError ? Error: {error.message} : null} +
    + {data.map((post) =>
  • {post.title}
  • )} +
+
+ ) } ``` @@ -75,7 +84,7 @@ function Posts() { function queryOptions(options): OmitKeyof, "queryFn"> & object & QueryKeyWithDataTag; ``` -Defined in: [preact-query/src/queryOptions.ts:201](https://github.com/TanStack/query/blob/main/packages/preact-query/src/queryOptions.ts#L201) +Defined in: [preact-query/src/queryOptions.ts:213](https://github.com/TanStack/query/blob/main/packages/preact-query/src/queryOptions.ts#L213) You can generally pass everything to `queryOptions` that you can also pass to `useQuery`. These options can be shared across hooks and imperative APIs such as `queryClient.query`. `options.queryKey` is required and @@ -117,15 +126,6 @@ The same options object, typed so that `queryKey` carries the inferred data type ### Examples -```tsx -import { queryOptions } from '@tanstack/preact-query' - -export const postsOptions = queryOptions({ - queryKey: ['posts'], - queryFn: fetchPosts, -}) -``` - A parameterized factory, reused across a hook and an imperative call with the same cache entry: ```tsx import { noop, queryOptions, useQuery } from '@tanstack/preact-query' @@ -137,30 +137,43 @@ export const postOptions = (id: string) => }) function Post({ id }: { id: string }) { - const { data } = useQuery(postOptions(id)) - return

{data?.title}

+ const { data, isPending, isError, error } = useQuery(postOptions(id)) + + if (isPending) return 'Loading...' + if (isError) return Error: {error.message} + + return

{data.title}

} -// Elsewhere, e.g. to warm the cache before rendering ``: -queryClient.query(postOptions(id)).catch(noop) +// `postOptions` also works with imperative APIs like `queryClient.query` — +// see `useQuery` for an example that warms the cache this way before rendering ``. +const postId = '1' +queryClient.query(postOptions(postId)).catch(noop) ``` The same options object works with every API that accepts query options: ```tsx -import { - noop, - queryOptions, - useQuery, - useSuspenseQuery, -} from '@tanstack/preact-query' +import { noop, queryOptions, useQuery } from '@tanstack/preact-query' const todosOptions = queryOptions({ queryKey: ['todos'], queryFn: fetchTodos, }) -useQuery(todosOptions) -useSuspenseQuery(todosOptions) +function Todos() { + const { data, isPending, isError, error } = useQuery(todosOptions) + + if (isPending) return 'Loading...' + if (isError) return Error: {error.message} + + return ( +
    + {data.map((todo) =>
  • {todo.title}
  • )} +
+ ) +} + +// The same options object works with the imperative APIs too: queryClient.query(todosOptions).catch(noop) queryClient.getQueryData(todosOptions.queryKey) // typed as Array | undefined ``` @@ -171,7 +184,7 @@ queryClient.getQueryData(todosOptions.queryKey) // typed as Array | undefi function queryOptions(options): UseQueryOptions & object & QueryKeyWithDataTag; ``` -Defined in: [preact-query/src/queryOptions.ts:271](https://github.com/TanStack/query/blob/main/packages/preact-query/src/queryOptions.ts#L271) +Defined in: [preact-query/src/queryOptions.ts:309](https://github.com/TanStack/query/blob/main/packages/preact-query/src/queryOptions.ts#L309) You can generally pass everything to `queryOptions` that you can also pass to `useQuery`. These options can be shared across hooks and imperative APIs such as `queryClient.query`. `options.queryKey` is required and @@ -211,16 +224,11 @@ The same options object, typed so that `queryKey` carries the inferred data type [useQuery](useQuery.md) to run a query with these options. -### Examples +### Remarks -```tsx -import { queryOptions } from '@tanstack/preact-query' +This is the only overload that accepts `queryFn: skipToken`, shown below. -export const postsOptions = queryOptions({ - queryKey: ['posts'], - queryFn: fetchPosts, -}) -``` +### Examples A parameterized factory, reused across a hook and an imperative call with the same cache entry: ```tsx @@ -233,30 +241,64 @@ export const postOptions = (id: string) => }) function Post({ id }: { id: string }) { - const { data } = useQuery(postOptions(id)) - return

{data?.title}

+ const { data, isPending, isError, error } = useQuery(postOptions(id)) + + if (isPending) return 'Loading...' + if (isError) return Error: {error.message} + + return

{data.title}

} -// Elsewhere, e.g. to warm the cache before rendering ``: -queryClient.query(postOptions(id)).catch(noop) +// `postOptions` also works with imperative APIs like `queryClient.query` — +// see `useQuery` for an example that warms the cache this way before rendering ``. +const postId = '1' +queryClient.query(postOptions(postId)).catch(noop) ``` The same options object works with every API that accepts query options: ```tsx -import { - noop, - queryOptions, - useQuery, - useSuspenseQuery, -} from '@tanstack/preact-query' +import { noop, queryOptions, useQuery } from '@tanstack/preact-query' const todosOptions = queryOptions({ queryKey: ['todos'], queryFn: fetchTodos, }) -useQuery(todosOptions) -useSuspenseQuery(todosOptions) +function Todos() { + const { data, isPending, isError, error } = useQuery(todosOptions) + + if (isPending) return 'Loading...' + if (isError) return Error: {error.message} + + return ( +
    + {data.map((todo) =>
  • {todo.title}
  • )} +
+ ) +} + +// The same options object works with the imperative APIs too: queryClient.query(todosOptions).catch(noop) queryClient.getQueryData(todosOptions.queryKey) // typed as Array | undefined ``` + +A factory that disables the query, type safe, until `postId` is set: +```tsx +import { queryOptions, skipToken, useQuery } from '@tanstack/preact-query' + +export const postOptions = (postId: number | undefined) => + queryOptions({ + queryKey: ['post', postId], + queryFn: postId != null ? () => fetchPost(postId) : skipToken, + }) + +function Post({ postId }: { postId: number | undefined }) { + const { data, isLoading, isError, error } = useQuery(postOptions(postId)) + + if (postId == null) return 'Select a post' + if (isLoading) return 'Loading...' + if (isError) return Error: {error.message} + + return

{data?.title}

+} +``` diff --git a/docs/framework/preact/reference/functions/useInfiniteQuery.md b/docs/framework/preact/reference/functions/useInfiniteQuery.md index 3eaadf977b..0201330bc3 100644 --- a/docs/framework/preact/reference/functions/useInfiniteQuery.md +++ b/docs/framework/preact/reference/functions/useInfiniteQuery.md @@ -9,7 +9,7 @@ title: useInfiniteQuery function useInfiniteQuery(options, queryClient?): DefinedUseInfiniteQueryResult; ``` -Defined in: [preact-query/src/useInfiniteQuery.ts:55](https://github.com/TanStack/query/blob/main/packages/preact-query/src/useInfiniteQuery.ts#L55) +Defined in: [preact-query/src/useInfiniteQuery.ts:64](https://github.com/TanStack/query/blob/main/packages/preact-query/src/useInfiniteQuery.ts#L64) The options for `useInfiniteQuery` are identical to `useQuery`, with the addition of `queryFn`, `initialPageParam`, `getNextPageParam`, `getPreviousPageParam`, and `maxPages`. @@ -77,7 +77,9 @@ actions, or add conditions like `hasNextPage && !isFetching`. import { useInfiniteQuery } from '@tanstack/preact-query' function Projects() { - const { data } = useInfiniteQuery({ + // `data` is never `undefined`, thanks to `initialData` — even if a refetch fails, so the + // list stays visible alongside the error. + const { data, isError, error } = useInfiniteQuery({ queryKey: ['projects'], queryFn: ({ pageParam }) => fetchProjects(pageParam), initialPageParam: 0, @@ -85,7 +87,14 @@ function Projects() { initialData: { pages: [], pageParams: [] }, }) - return <>{data.pages.map((page) => page.projects.map((p) =>

{p.name}

))} + return ( +
+ {isError ? Error: {error.message} : null} +
    + {data.pages.map((page) => page.projects.map((p) =>
  • {p.name}
  • ))} +
+
+ ) } ``` @@ -95,7 +104,7 @@ function Projects() { function useInfiniteQuery(options, queryClient?): UseInfiniteQueryResult; ``` -Defined in: [preact-query/src/useInfiniteQuery.ts:115](https://github.com/TanStack/query/blob/main/packages/preact-query/src/useInfiniteQuery.ts#L115) +Defined in: [preact-query/src/useInfiniteQuery.ts:189](https://github.com/TanStack/query/blob/main/packages/preact-query/src/useInfiniteQuery.ts#L189) The options for `useInfiniteQuery` are identical to `useQuery`, with the addition of `queryFn`, `initialPageParam`, `getNextPageParam`, `getPreviousPageParam`, and `maxPages`. @@ -155,13 +164,14 @@ actions, or add conditions like `hasNextPage && !isFetching`. [infiniteQueryOptions](infiniteQueryOptions.md) to share these options between `useInfiniteQuery` and imperative APIs like `queryClient.infiniteQuery`. -### Example +### Examples +Fetching the next page from a "Load More" button click: ```tsx import { useInfiniteQuery } from '@tanstack/preact-query' function Projects() { - const { data, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage } = + const { data, isPending, isError, error, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage } = useInfiniteQuery({ queryKey: ['projects'], queryFn: ({ pageParam }) => fetchProjects(pageParam), @@ -169,17 +179,80 @@ function Projects() { getNextPageParam: (lastPage) => lastPage.nextId, }) + if (isPending) return 'Loading...' + if (isError) return Error: {error.message} + return ( - + <> +
    + {data.pages.map((page) => + page.projects.map((project) =>
  • {project.name}
  • ), + )} +
+ + + ) +} +``` + +Fetching the next page automatically as the user scrolls, using an `IntersectionObserver` on a +sentinel element after the list: +```tsx +import { useInfiniteQuery } from '@tanstack/preact-query' +import { useEffect, useRef } from 'preact/hooks' + +function Projects() { + const { + data, + isPending, + isError, + error, + fetchNextPage, + hasNextPage, + isFetching, + isFetchingNextPage, + } = useInfiniteQuery({ + queryKey: ['projects'], + queryFn: ({ pageParam }) => fetchProjects(pageParam), + initialPageParam: 0, + getNextPageParam: (lastPage) => lastPage.nextId, + }) + + const sentinelRef = useRef(null) + + useEffect(() => { + const sentinel = sentinelRef.current + if (sentinel == null || !hasNextPage || isFetching) return + + const observer = new IntersectionObserver(([entry]) => { + if (entry?.isIntersecting) fetchNextPage() + }) + observer.observe(sentinel) + + return () => observer.disconnect() + }, [hasNextPage, isFetching, fetchNextPage]) + + if (isPending) return 'Loading...' + if (isError) return Error: {error.message} + + return ( + <> +
    + {data.pages.map((page) => + page.projects.map((project) =>
  • {project.name}
  • ), + )} +
+
{isFetchingNextPage ? 'Loading more...' : null}
+ ) } ``` @@ -190,7 +263,7 @@ function Projects() { function useInfiniteQuery(options, queryClient?): UseInfiniteQueryResult; ``` -Defined in: [preact-query/src/useInfiniteQuery.ts:175](https://github.com/TanStack/query/blob/main/packages/preact-query/src/useInfiniteQuery.ts#L175) +Defined in: [preact-query/src/useInfiniteQuery.ts:390](https://github.com/TanStack/query/blob/main/packages/preact-query/src/useInfiniteQuery.ts#L390) The options for `useInfiniteQuery` are identical to `useQuery`, with the addition of `queryFn`, `initialPageParam`, `getNextPageParam`, `getPreviousPageParam`, and `maxPages`. @@ -250,13 +323,14 @@ actions, or add conditions like `hasNextPage && !isFetching`. [infiniteQueryOptions](infiniteQueryOptions.md) to share these options between `useInfiniteQuery` and imperative APIs like `queryClient.infiniteQuery`. -### Example +### Examples +Fetching the next page from a "Load More" button click: ```tsx import { useInfiniteQuery } from '@tanstack/preact-query' function Projects() { - const { data, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage } = + const { data, isPending, isError, error, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage } = useInfiniteQuery({ queryKey: ['projects'], queryFn: ({ pageParam }) => fetchProjects(pageParam), @@ -264,17 +338,154 @@ function Projects() { getNextPageParam: (lastPage) => lastPage.nextId, }) + if (isPending) return 'Loading...' + if (isError) return Error: {error.message} + return ( - + + ) +} +``` + +Fetching the next page automatically as the user scrolls, using an `IntersectionObserver` on a +sentinel element after the list: +```tsx +import { useInfiniteQuery } from '@tanstack/preact-query' +import { useEffect, useRef } from 'preact/hooks' + +function Projects() { + const { + data, + isPending, + isError, + error, + fetchNextPage, + hasNextPage, + isFetching, + isFetchingNextPage, + } = useInfiniteQuery({ + queryKey: ['projects'], + queryFn: ({ pageParam }) => fetchProjects(pageParam), + initialPageParam: 0, + getNextPageParam: (lastPage) => lastPage.nextId, + }) + + const sentinelRef = useRef(null) + + useEffect(() => { + const sentinel = sentinelRef.current + if (sentinel == null || !hasNextPage || isFetching) return + + const observer = new IntersectionObserver(([entry]) => { + if (entry?.isIntersecting) fetchNextPage() + }) + observer.observe(sentinel) + + return () => observer.disconnect() + }, [hasNextPage, isFetching, fetchNextPage]) + + if (isPending) return 'Loading...' + if (isError) return Error: {error.message} + + return ( + <> +
    + {data.pages.map((page) => + page.projects.map((project) =>
  • {project.name}
  • ), + )} +
+
{isFetchingNextPage ? 'Loading more...' : null}
+ + ) +} +``` + +Warming the cache on hover, so `` has data as soon as it's clicked. Requires an +[infiniteQueryOptions](infiniteQueryOptions.md) factory, so the hook and the imperative call share the same cache entry: +```tsx +import { + infiniteQueryOptions, + noop, + useInfiniteQuery, + useQueryClient, +} from '@tanstack/preact-query' + +const commentsOptions = (postId: string) => + infiniteQueryOptions({ + queryKey: ['post', postId, 'comments'], + queryFn: ({ pageParam }) => fetchComments(postId, pageParam), + initialPageParam: 0, + getNextPageParam: (lastPage) => lastPage.nextId, + }) + +function Comments({ postId }: { postId: string }) { + const { data, isPending, isError, error } = useInfiniteQuery(commentsOptions(postId)) + + if (isPending) return 'Loading...' + if (isError) return Error: {error.message} + + return ( +
    + {data.pages.map((page) => page.comments.map((c) =>
  • {c.text}
  • ))} +
+ ) +} + +function PostLink({ postId, title }: { postId: string; title: string }) { + const queryClient = useQueryClient() + + return ( + queryClient.infiniteQuery(commentsOptions(postId)).catch(noop)} > - {isFetchingNextPage - ? 'Loading more...' - : hasNextPage - ? 'Load More' - : 'Nothing more to load'} - + {title} + + ) +} +``` + +A query that's disabled, type safe, until `postId` is set — pass `skipToken` as `queryFn` +instead of setting `enabled: false`: +```tsx +import { skipToken, useInfiniteQuery } from '@tanstack/preact-query' + +function Comments({ postId }: { postId: string | undefined }) { + // Use `isLoading`, not `isPending`, so the loading state doesn't show while the query is disabled. + const { data, isLoading, isError, error } = useInfiniteQuery({ + queryKey: ['post', postId, 'comments'], + queryFn: + postId != null + ? ({ pageParam }) => fetchComments(postId, pageParam) + : skipToken, + initialPageParam: 0, + getNextPageParam: (lastPage) => lastPage.nextId, + }) + + if (postId == null) return 'Select a post' + if (isLoading) return 'Loading...' + if (isError) return Error: {error.message} + + return ( +
    + {data?.pages.map((page) => page.comments.map((c) =>
  • {c.text}
  • ))} +
) } ``` diff --git a/docs/framework/preact/reference/functions/useIsFetching.md b/docs/framework/preact/reference/functions/useIsFetching.md index 75aea1a394..915c55fab0 100644 --- a/docs/framework/preact/reference/functions/useIsFetching.md +++ b/docs/framework/preact/reference/functions/useIsFetching.md @@ -7,7 +7,7 @@ title: useIsFetching function useIsFetching(filters?, queryClient?): number; ``` -Defined in: [preact-query/src/useIsFetching.ts:42](https://github.com/TanStack/query/blob/main/packages/preact-query/src/useIsFetching.ts#L42) +Defined in: [preact-query/src/useIsFetching.ts:44](https://github.com/TanStack/query/blob/main/packages/preact-query/src/useIsFetching.ts#L44) `useIsFetching` is an optional hook that returns the `number` of the queries that your application is loading or fetching in the background (useful for app-wide loading indicators). @@ -39,10 +39,12 @@ background. ```tsx import { useIsFetching } from '@tanstack/preact-query' -// How many queries are fetching? -const isFetching = useIsFetching() -// How many queries matching the posts prefix are fetching? -const isFetchingPosts = useIsFetching({ queryKey: ['posts'] }) +function PostsFetchingIndicator() { + // How many queries matching the posts prefix are fetching? + const isFetchingPosts = useIsFetching({ queryKey: ['posts'] }) + + return isFetchingPosts ? Refreshing posts... : null +} ``` A global loading indicator for any query fetching in the background, not just the ones on screen: diff --git a/docs/framework/preact/reference/functions/useIsMutating.md b/docs/framework/preact/reference/functions/useIsMutating.md index 941496a00b..bfc97539bd 100644 --- a/docs/framework/preact/reference/functions/useIsMutating.md +++ b/docs/framework/preact/reference/functions/useIsMutating.md @@ -7,7 +7,7 @@ title: useIsMutating function useIsMutating(filters?, queryClient?): number; ``` -Defined in: [preact-query/src/useMutationState.ts:33](https://github.com/TanStack/query/blob/main/packages/preact-query/src/useMutationState.ts#L33) +Defined in: [preact-query/src/useMutationState.ts:35](https://github.com/TanStack/query/blob/main/packages/preact-query/src/useMutationState.ts#L35) `useIsMutating` is an optional hook that returns the `number` of mutations that your application is fetching (useful for app-wide loading indicators). @@ -38,8 +38,10 @@ Will be the `number` of the mutations that your application is currently fetchin ```tsx import { useIsMutating } from '@tanstack/preact-query' -// How many mutations are fetching? -const isMutating = useIsMutating() -// How many mutations matching the posts prefix are fetching? -const isMutatingPosts = useIsMutating({ mutationKey: ['posts'] }) +function PostsMutatingIndicator() { + // How many mutations matching the posts prefix are in progress? + const isMutatingPosts = useIsMutating({ mutationKey: ['posts'] }) + + return isMutatingPosts ? Saving posts... : null +} ``` diff --git a/docs/framework/preact/reference/functions/useMutation.md b/docs/framework/preact/reference/functions/useMutation.md index f01293841b..6483a6b004 100644 --- a/docs/framework/preact/reference/functions/useMutation.md +++ b/docs/framework/preact/reference/functions/useMutation.md @@ -7,7 +7,7 @@ title: useMutation function useMutation(options, queryClient?): UseMutationResult; ``` -Defined in: [preact-query/src/useMutation.ts:190](https://github.com/TanStack/query/blob/main/packages/preact-query/src/useMutation.ts#L190) +Defined in: [preact-query/src/useMutation.ts:191](https://github.com/TanStack/query/blob/main/packages/preact-query/src/useMutation.ts#L191) Unlike queries, mutations are typically used to create/update/delete data or perform server side-effects. `useMutation` is the hook for that. @@ -56,7 +56,8 @@ made. ## See -[mutationOptions](mutationOptions.md) to share these options across multiple `useMutation` call sites. +[mutationOptions](mutationOptions.md) to share these options across multiple `useMutation` call sites, or to look +the mutation up elsewhere via its `mutationKey` (e.g. with `useMutationState`). ## Examples @@ -193,13 +194,13 @@ function AddTodos() { }) async function handleAddAll(todos: Array) { - const results = await Promise.allSettled( + const addResults = await Promise.allSettled( todos.map((todo) => addMutation.mutateAsync(todo)), ) - results.forEach((result, index) => { - if (result.status === 'rejected') { - console.error(`Failed to add "${todos[index]}":`, result.reason) + addResults.forEach((addResult, index) => { + if (addResult.status === 'rejected') { + console.error(`Failed to add "${todos[index]}":`, addResult.reason) } }) } diff --git a/docs/framework/preact/reference/functions/useMutationState.md b/docs/framework/preact/reference/functions/useMutationState.md index 6288bff6ef..37bee7a2f6 100644 --- a/docs/framework/preact/reference/functions/useMutationState.md +++ b/docs/framework/preact/reference/functions/useMutationState.md @@ -7,7 +7,7 @@ title: useMutationState function useMutationState(options, queryClient?): TResult[]; ``` -Defined in: [preact-query/src/useMutationState.ts:137](https://github.com/TanStack/query/blob/main/packages/preact-query/src/useMutationState.ts#L137) +Defined in: [preact-query/src/useMutationState.ts:157](https://github.com/TanStack/query/blob/main/packages/preact-query/src/useMutationState.ts#L157) `useMutationState` is a hook that gives you access to all mutations in the `MutationCache`. You can pass `filters` (MutationFilters) to narrow down your mutations, and `select` to transform the mutation @@ -51,10 +51,14 @@ Get all variables of all running mutations: ```tsx import { useMutationState } from '@tanstack/preact-query' -const variables = useMutationState({ - filters: { status: 'pending' }, - select: (mutation) => mutation.state.variables, -}) +function PendingPosts() { + const pendingVariables = useMutationState({ + filters: { status: 'pending' }, + select: (mutation) => mutation.state.variables, + }) + + return <>{pendingVariables.length} posts saving... +} ``` Get all data for specific mutations via the `mutationKey`: @@ -63,27 +67,41 @@ import { useMutation, useMutationState } from '@tanstack/preact-query' const mutationKey = ['posts'] -// Some mutation that we want to get the state for -const mutation = useMutation({ - mutationKey, - mutationFn: createPosts, -}) - -const data = useMutationState({ - // this mutation key needs to match the mutation key of the given mutation (see above) - filters: { mutationKey }, - select: (mutation) => mutation.state.data, -}) +function Posts() { + // Some mutation that we want to get the state for + const mutation = useMutation({ + mutationKey, + mutationFn: createPosts, + }) + + const savedPosts = useMutationState({ + // this mutation key needs to match the mutation key of the given mutation (see above) + filters: { mutationKey, status: 'success' }, + select: (mutation) => mutation.state.data, + }) + + return ( + + ) +} ``` Access the latest mutation data via the `mutationKey`. Each invocation of `mutate` adds a new entry to the mutation cache for `gcTime` milliseconds — check the last item that `useMutationState` returns to get the latest invocation: ```tsx -const data = useMutationState({ - filters: { mutationKey: ['posts'] }, - select: (mutation) => mutation.state.data, -}) +import { useMutationState } from '@tanstack/preact-query' + +function LatestPost() { + const savedPosts = useMutationState({ + filters: { mutationKey: ['posts'], status: 'success' }, + select: (mutation) => mutation.state.data, + }) + + const latestSavedPost = savedPosts[savedPosts.length - 1] -const latest = data[data.length - 1] + return <>{latestSavedPost ? 'Saved' : 'Nothing saved yet'} +} ``` diff --git a/docs/framework/preact/reference/functions/useQueries.md b/docs/framework/preact/reference/functions/useQueries.md index 154077759f..668211e1d3 100644 --- a/docs/framework/preact/reference/functions/useQueries.md +++ b/docs/framework/preact/reference/functions/useQueries.md @@ -7,7 +7,7 @@ title: useQueries function useQueries(__namedParameters, queryClient?): TCombinedResult; ``` -Defined in: [preact-query/src/useQueries.ts:275](https://github.com/TanStack/query/blob/main/packages/preact-query/src/useQueries.ts#L275) +Defined in: [preact-query/src/useQueries.ts:301](https://github.com/TanStack/query/blob/main/packages/preact-query/src/useQueries.ts#L301) The `useQueries` hook can be used to fetch a variable number of queries. @@ -83,29 +83,55 @@ order as the input. When `combine` is provided, this is the value returned by `c ```tsx import { useQueries } from '@tanstack/preact-query' -const ids = [1, 2, 3] -const results = useQueries({ - queries: ids.map((id) => ({ - queryKey: ['post', id], - queryFn: () => fetchPost(id), - staleTime: Infinity, - })), -}) +function Posts({ ids }: { ids: Array }) { + const postQueries = useQueries({ + queries: ids.map((id) => ({ + queryKey: ['post', id], + queryFn: () => fetchPost(id), + staleTime: Infinity, + })), + }) + + return ( +
    + {postQueries.map((query, index) => { + if (query.isPending) return
  • Loading...
  • + if (query.isError) return
  • Error: {query.error.message}
  • + return
  • {query.data.title}
  • + })} +
+ ) +} ``` Combining results into a single value: ```tsx -const ids = [1, 2, 3] -const combinedQueries = useQueries({ - queries: ids.map((id) => ({ - queryKey: ['post', id], - queryFn: () => fetchPost(id), - })), - combine: (results) => { - return { - data: results.map((result) => result.data), - pending: results.some((result) => result.isPending), - } - }, -}) +import { useQueries } from '@tanstack/preact-query' + +function Posts({ ids }: { ids: Array }) { + const { data, isPending, isError } = useQueries({ + queries: ids.map((id) => ({ + queryKey: ['post', id], + queryFn: () => fetchPost(id), + })), + combine: (postQueries) => { + return { + data: postQueries.map((query) => query.data), + isPending: postQueries.some((query) => query.isPending), + isError: postQueries.some((query) => query.isError), + } + }, + }) + + if (isPending) return 'Loading...' + if (isError) return 'Error loading posts' + + return ( +
    + {data.map((post) => ( +
  • {post?.title}
  • + ))} +
+ ) +} ``` diff --git a/docs/framework/preact/reference/functions/useQuery.md b/docs/framework/preact/reference/functions/useQuery.md index c9d8c943d1..4ce44660c4 100644 --- a/docs/framework/preact/reference/functions/useQuery.md +++ b/docs/framework/preact/reference/functions/useQuery.md @@ -9,7 +9,7 @@ title: useQuery function useQuery(options, queryClient?): DefinedUseQueryResult; ``` -Defined in: [preact-query/src/useQuery.ts:42](https://github.com/TanStack/query/blob/main/packages/preact-query/src/useQuery.ts#L42) +Defined in: [preact-query/src/useQuery.ts:50](https://github.com/TanStack/query/blob/main/packages/preact-query/src/useQuery.ts#L50) This overload is selected when `initialData` is set, so the resulting `data` is never `undefined`. @@ -64,14 +64,22 @@ since `initialData` guarantees data upfront). `isSuccess`/`isError` are derived import { useQuery } from '@tanstack/preact-query' function Posts() { - // `data` is `Post[]`, never `undefined`, thanks to `initialData`. - const { data } = useQuery({ + // `data` is `Post[]`, never `undefined`, thanks to `initialData` — even if a refetch fails, + // so the list stays visible alongside the error. + const { data, isError, error } = useQuery({ queryKey: ['posts'], queryFn: fetchPosts, initialData: [], }) - return <>{data.map((post) =>

{post.title}

)} + return ( +
+ {isError ? Error: {error.message} : null} +
    + {data.map((post) =>
  • {post.title}
  • )} +
+
+ ) } ``` @@ -81,7 +89,7 @@ function Posts() { function useQuery(options, queryClient?): UseQueryResult; ``` -Defined in: [preact-query/src/useQuery.ts:105](https://github.com/TanStack/query/blob/main/packages/preact-query/src/useQuery.ts#L105) +Defined in: [preact-query/src/useQuery.ts:119](https://github.com/TanStack/query/blob/main/packages/preact-query/src/useQuery.ts#L119) ### Type Parameters @@ -146,9 +154,11 @@ function Posts() { return (
- {data.map((post) => ( -

{post.title}

- ))} +
    + {data.map((post) => ( +
  • {post.title}
  • + ))} +
{isFetching ? 'Background Updating...' : ' '}
) @@ -168,7 +178,11 @@ function Posts() { if (isPending) return 'Loading...' if (isError) return Error: {error.message} - return <>{data.map((post) =>

{post.title}

)} + return ( +
    + {data.map((post) =>
  • {post.title}
  • )} +
+ ) } ``` @@ -178,7 +192,7 @@ function Posts() { function useQuery(options, queryClient?): UseQueryResult; ``` -Defined in: [preact-query/src/useQuery.ts:221](https://github.com/TanStack/query/blob/main/packages/preact-query/src/useQuery.ts#L221) +Defined in: [preact-query/src/useQuery.ts:297](https://github.com/TanStack/query/blob/main/packages/preact-query/src/useQuery.ts#L297) ### Type Parameters @@ -243,9 +257,11 @@ function Posts() { return (
- {data.map((post) => ( -

{post.title}

- ))} +
    + {data.map((post) => ( +
  • {post.title}
  • + ))} +
{isFetching ? 'Background Updating...' : ' '}
) @@ -272,6 +288,27 @@ function Post({ postId }: { postId: number | undefined }) { } ``` +The same dependent query, type safe: `skipToken` disables the query without needing the +non-null assertion above, since `queryFn` is only ever called when `postId` is defined. +`refetch` doesn't work while `queryFn` is `skipToken` — use `enabled: false` instead if you +need to trigger the query manually: +```tsx +import { skipToken, useQuery } from '@tanstack/preact-query' + +function Post({ postId }: { postId: number | undefined }) { + const { data, isLoading, isError, error } = useQuery({ + queryKey: ['post', postId], + queryFn: postId != null ? () => fetchPost(postId) : skipToken, + }) + + if (postId == null) return 'Select a post' + if (isLoading) return 'Loading...' + if (isError) return Error: {error.message} + + return

{data?.title}

+} +``` + Seeding a detail query from an already-cached list, to skip the loading state: ```tsx import { useQuery, useQueryClient } from '@tanstack/preact-query' @@ -279,7 +316,7 @@ import { useQuery, useQueryClient } from '@tanstack/preact-query' function Post({ postId }: { postId: number }) { const queryClient = useQueryClient() - const { data } = useQuery({ + const { data, isError, error } = useQuery({ queryKey: ['post', postId], queryFn: () => fetchPost(postId), initialData: () => @@ -288,6 +325,8 @@ function Post({ postId }: { postId: number }) { ?.find((post) => post.id === postId), }) + if (isError) return Error: {error.message} + return

{data?.title}

} ``` @@ -300,15 +339,19 @@ import { useState } from 'preact/hooks' function Posts() { const [page, setPage] = useState(0) - const { data, isPlaceholderData } = useQuery({ + const { data, isPlaceholderData, isError, error } = useQuery({ queryKey: ['posts', page], queryFn: () => fetchPosts(page), placeholderData: keepPreviousData, }) + if (isError) return Error: {error.message} + return (
- {data?.map((post) =>

{post.title}

)} +
    + {data?.map((post) =>
  • {post.title}
  • )} +
- * } - * ``` - * - * @example * Looking the mutation up elsewhere via its `mutationKey`, e.g. for a global "saving…" indicator: * ```tsx * import { mutationOptions, useMutationState } from '@tanstack/preact-query' @@ -69,6 +54,8 @@ export function mutationOptions< * @param options - The mutation options to use, identical to what you'd pass to `useMutation`, without a * `mutationKey`. * @returns The same options object, unchanged. + * @remarks Without a `mutationKey`, the mutation can't be looked up elsewhere via `useMutationState` — see + * the other overload's example for that. * * @example * ```tsx diff --git a/packages/preact-query/src/queryOptions.ts b/packages/preact-query/src/queryOptions.ts index 440306fce4..ef9aec009f 100644 --- a/packages/preact-query/src/queryOptions.ts +++ b/packages/preact-query/src/queryOptions.ts @@ -122,9 +122,18 @@ export type DefinedInitialDataOptions< * }) * * function Posts() { - * // `data` is `Post[]`, never `undefined`, thanks to `initialData`. - * const { data } = useQuery(postsOptions) - * return <>{data.map((post) =>

{post.title}

)} + * // `data` is `Post[]`, never `undefined`, thanks to `initialData` — even if a refetch fails, + * // so the list stays visible alongside the error. + * const { data, isError, error } = useQuery(postsOptions) + * + * return ( + *
+ * {isError ? Error: {error.message} : null} + *
    + * {data.map((post) =>
  • {post.title}
  • )} + *
+ *
+ * ) * } * ``` */ @@ -148,16 +157,6 @@ export function queryOptions< * @returns The same options object, typed so that `queryKey` carries the inferred data type. * * @example - * ```tsx - * import { queryOptions } from '@tanstack/preact-query' - * - * export const postsOptions = queryOptions({ - * queryKey: ['posts'], - * queryFn: fetchPosts, - * }) - * ``` - * - * @example * A parameterized factory, reused across a hook and an imperative call with the same cache entry: * ```tsx * import { noop, queryOptions, useQuery } from '@tanstack/preact-query' @@ -169,31 +168,44 @@ export function queryOptions< * }) * * function Post({ id }: { id: string }) { - * const { data } = useQuery(postOptions(id)) - * return

{data?.title}

+ * const { data, isPending, isError, error } = useQuery(postOptions(id)) + * + * if (isPending) return 'Loading...' + * if (isError) return Error: {error.message} + * + * return

{data.title}

* } * - * // Elsewhere, e.g. to warm the cache before rendering ``: - * queryClient.query(postOptions(id)).catch(noop) + * // `postOptions` also works with imperative APIs like `queryClient.query` — + * // see `useQuery` for an example that warms the cache this way before rendering ``. + * const postId = '1' + * queryClient.query(postOptions(postId)).catch(noop) * ``` * * @example * The same options object works with every API that accepts query options: * ```tsx - * import { - * noop, - * queryOptions, - * useQuery, - * useSuspenseQuery, - * } from '@tanstack/preact-query' + * import { noop, queryOptions, useQuery } from '@tanstack/preact-query' * * const todosOptions = queryOptions({ * queryKey: ['todos'], * queryFn: fetchTodos, * }) * - * useQuery(todosOptions) - * useSuspenseQuery(todosOptions) + * function Todos() { + * const { data, isPending, isError, error } = useQuery(todosOptions) + * + * if (isPending) return 'Loading...' + * if (isError) return Error: {error.message} + * + * return ( + *
    + * {data.map((todo) =>
  • {todo.title}
  • )} + *
+ * ) + * } + * + * // The same options object works with the imperative APIs too: * queryClient.query(todosOptions).catch(noop) * queryClient.getQueryData(todosOptions.queryKey) // typed as Array | undefined * ``` @@ -216,16 +228,7 @@ export function queryOptions< * @see {@link useQuery} to run a query with these options. * @param options - The {@link UndefinedInitialDataOptions} to use — everything you can pass to `useQuery`. * @returns The same options object, typed so that `queryKey` carries the inferred data type. - * - * @example - * ```tsx - * import { queryOptions } from '@tanstack/preact-query' - * - * export const postsOptions = queryOptions({ - * queryKey: ['posts'], - * queryFn: fetchPosts, - * }) - * ``` + * @remarks This is the only overload that accepts `queryFn: skipToken`, shown below. * * @example * A parameterized factory, reused across a hook and an imperative call with the same cache entry: @@ -239,34 +242,69 @@ export function queryOptions< * }) * * function Post({ id }: { id: string }) { - * const { data } = useQuery(postOptions(id)) - * return

{data?.title}

+ * const { data, isPending, isError, error } = useQuery(postOptions(id)) + * + * if (isPending) return 'Loading...' + * if (isError) return Error: {error.message} + * + * return

{data.title}

* } * - * // Elsewhere, e.g. to warm the cache before rendering ``: - * queryClient.query(postOptions(id)).catch(noop) + * // `postOptions` also works with imperative APIs like `queryClient.query` — + * // see `useQuery` for an example that warms the cache this way before rendering ``. + * const postId = '1' + * queryClient.query(postOptions(postId)).catch(noop) * ``` * * @example * The same options object works with every API that accepts query options: * ```tsx - * import { - * noop, - * queryOptions, - * useQuery, - * useSuspenseQuery, - * } from '@tanstack/preact-query' + * import { noop, queryOptions, useQuery } from '@tanstack/preact-query' * * const todosOptions = queryOptions({ * queryKey: ['todos'], * queryFn: fetchTodos, * }) * - * useQuery(todosOptions) - * useSuspenseQuery(todosOptions) + * function Todos() { + * const { data, isPending, isError, error } = useQuery(todosOptions) + * + * if (isPending) return 'Loading...' + * if (isError) return Error: {error.message} + * + * return ( + *
    + * {data.map((todo) =>
  • {todo.title}
  • )} + *
+ * ) + * } + * + * // The same options object works with the imperative APIs too: * queryClient.query(todosOptions).catch(noop) * queryClient.getQueryData(todosOptions.queryKey) // typed as Array | undefined * ``` + * + * @example + * A factory that disables the query, type safe, until `postId` is set: + * ```tsx + * import { queryOptions, skipToken, useQuery } from '@tanstack/preact-query' + * + * export const postOptions = (postId: number | undefined) => + * queryOptions({ + * queryKey: ['post', postId], + * queryFn: postId != null ? () => fetchPost(postId) : skipToken, + * }) + * + * function Post({ postId }: { postId: number | undefined }) { + * const { data, isLoading, isError, error } = useQuery(postOptions(postId)) + * + * if (postId == null) return 'Select a post' + * if (isLoading) return 'Loading...' + * if (isError) return Error: {error.message} + * + * return

{data?.title}

+ * } + * ``` */ export function queryOptions< TQueryFnData = unknown, diff --git a/packages/preact-query/src/useInfiniteQuery.ts b/packages/preact-query/src/useInfiniteQuery.ts index a525029ca7..36e1514d37 100644 --- a/packages/preact-query/src/useInfiniteQuery.ts +++ b/packages/preact-query/src/useInfiniteQuery.ts @@ -40,7 +40,9 @@ import { useBaseQuery } from './useBaseQuery' * import { useInfiniteQuery } from '@tanstack/preact-query' * * function Projects() { - * const { data } = useInfiniteQuery({ + * // `data` is never `undefined`, thanks to `initialData` — even if a refetch fails, so the + * // list stays visible alongside the error. + * const { data, isError, error } = useInfiniteQuery({ * queryKey: ['projects'], * queryFn: ({ pageParam }) => fetchProjects(pageParam), * initialPageParam: 0, @@ -48,7 +50,14 @@ import { useBaseQuery } from './useBaseQuery' * initialData: { pages: [], pageParams: [] }, * }) * - * return <>{data.pages.map((page) => page.projects.map((p) =>

{p.name}

))} + * return ( + *
+ * {isError ? Error: {error.message} : null} + *
    + * {data.pages.map((page) => page.projects.map((p) =>
  • {p.name}
  • ))} + *
+ *
+ * ) * } * ``` */ @@ -85,11 +94,12 @@ export function useInfiniteQuery< * `isFetchingPreviousPage`. * * @example + * Fetching the next page from a "Load More" button click: * ```tsx * import { useInfiniteQuery } from '@tanstack/preact-query' * * function Projects() { - * const { data, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage } = + * const { data, isPending, isError, error, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage } = * useInfiniteQuery({ * queryKey: ['projects'], * queryFn: ({ pageParam }) => fetchProjects(pageParam), @@ -97,17 +107,81 @@ export function useInfiniteQuery< * getNextPageParam: (lastPage) => lastPage.nextId, * }) * + * if (isPending) return 'Loading...' + * if (isError) return Error: {error.message} + * * return ( - * + * <> + *
    + * {data.pages.map((page) => + * page.projects.map((project) =>
  • {project.name}
  • ), + * )} + *
+ * + * + * ) + * } + * ``` + * + * @example + * Fetching the next page automatically as the user scrolls, using an `IntersectionObserver` on a + * sentinel element after the list: + * ```tsx + * import { useInfiniteQuery } from '@tanstack/preact-query' + * import { useEffect, useRef } from 'preact/hooks' + * + * function Projects() { + * const { + * data, + * isPending, + * isError, + * error, + * fetchNextPage, + * hasNextPage, + * isFetching, + * isFetchingNextPage, + * } = useInfiniteQuery({ + * queryKey: ['projects'], + * queryFn: ({ pageParam }) => fetchProjects(pageParam), + * initialPageParam: 0, + * getNextPageParam: (lastPage) => lastPage.nextId, + * }) + * + * const sentinelRef = useRef(null) + * + * useEffect(() => { + * const sentinel = sentinelRef.current + * if (sentinel == null || !hasNextPage || isFetching) return + * + * const observer = new IntersectionObserver(([entry]) => { + * if (entry?.isIntersecting) fetchNextPage() + * }) + * observer.observe(sentinel) + * + * return () => observer.disconnect() + * }, [hasNextPage, isFetching, fetchNextPage]) + * + * if (isPending) return 'Loading...' + * if (isError) return Error: {error.message} + * + * return ( + * <> + *
    + * {data.pages.map((page) => + * page.projects.map((project) =>
  • {project.name}
  • ), + * )} + *
+ *
{isFetchingNextPage ? 'Loading more...' : null}
+ * * ) * } * ``` @@ -145,11 +219,12 @@ export function useInfiniteQuery< * `isFetchingPreviousPage`. * * @example + * Fetching the next page from a "Load More" button click: * ```tsx * import { useInfiniteQuery } from '@tanstack/preact-query' * * function Projects() { - * const { data, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage } = + * const { data, isPending, isError, error, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage } = * useInfiniteQuery({ * queryKey: ['projects'], * queryFn: ({ pageParam }) => fetchProjects(pageParam), @@ -157,17 +232,157 @@ export function useInfiniteQuery< * getNextPageParam: (lastPage) => lastPage.nextId, * }) * + * if (isPending) return 'Loading...' + * if (isError) return Error: {error.message} + * * return ( - * + * + * ) + * } + * ``` + * + * @example + * Fetching the next page automatically as the user scrolls, using an `IntersectionObserver` on a + * sentinel element after the list: + * ```tsx + * import { useInfiniteQuery } from '@tanstack/preact-query' + * import { useEffect, useRef } from 'preact/hooks' + * + * function Projects() { + * const { + * data, + * isPending, + * isError, + * error, + * fetchNextPage, + * hasNextPage, + * isFetching, + * isFetchingNextPage, + * } = useInfiniteQuery({ + * queryKey: ['projects'], + * queryFn: ({ pageParam }) => fetchProjects(pageParam), + * initialPageParam: 0, + * getNextPageParam: (lastPage) => lastPage.nextId, + * }) + * + * const sentinelRef = useRef(null) + * + * useEffect(() => { + * const sentinel = sentinelRef.current + * if (sentinel == null || !hasNextPage || isFetching) return + * + * const observer = new IntersectionObserver(([entry]) => { + * if (entry?.isIntersecting) fetchNextPage() + * }) + * observer.observe(sentinel) + * + * return () => observer.disconnect() + * }, [hasNextPage, isFetching, fetchNextPage]) + * + * if (isPending) return 'Loading...' + * if (isError) return Error: {error.message} + * + * return ( + * <> + *
    + * {data.pages.map((page) => + * page.projects.map((project) =>
  • {project.name}
  • ), + * )} + *
+ *
{isFetchingNextPage ? 'Loading more...' : null}
+ * + * ) + * } + * ``` + * + * @example + * Warming the cache on hover, so `` has data as soon as it's clicked. Requires an + * {@link infiniteQueryOptions} factory, so the hook and the imperative call share the same cache entry: + * ```tsx + * import { + * infiniteQueryOptions, + * noop, + * useInfiniteQuery, + * useQueryClient, + * } from '@tanstack/preact-query' + * + * const commentsOptions = (postId: string) => + * infiniteQueryOptions({ + * queryKey: ['post', postId, 'comments'], + * queryFn: ({ pageParam }) => fetchComments(postId, pageParam), + * initialPageParam: 0, + * getNextPageParam: (lastPage) => lastPage.nextId, + * }) + * + * function Comments({ postId }: { postId: string }) { + * const { data, isPending, isError, error } = useInfiniteQuery(commentsOptions(postId)) + * + * if (isPending) return 'Loading...' + * if (isError) return Error: {error.message} + * + * return ( + *
    + * {data.pages.map((page) => page.comments.map((c) =>
  • {c.text}
  • ))} + *
+ * ) + * } + * + * function PostLink({ postId, title }: { postId: string; title: string }) { + * const queryClient = useQueryClient() + * + * return ( + * queryClient.infiniteQuery(commentsOptions(postId)).catch(noop)} * > - * {isFetchingNextPage - * ? 'Loading more...' - * : hasNextPage - * ? 'Load More' - * : 'Nothing more to load'} - * + * {title} + * + * ) + * } + * ``` + * + * @example + * A query that's disabled, type safe, until `postId` is set — pass `skipToken` as `queryFn` + * instead of setting `enabled: false`: + * ```tsx + * import { skipToken, useInfiniteQuery } from '@tanstack/preact-query' + * + * function Comments({ postId }: { postId: string | undefined }) { + * // Use `isLoading`, not `isPending`, so the loading state doesn't show while the query is disabled. + * const { data, isLoading, isError, error } = useInfiniteQuery({ + * queryKey: ['post', postId, 'comments'], + * queryFn: + * postId != null + * ? ({ pageParam }) => fetchComments(postId, pageParam) + * : skipToken, + * initialPageParam: 0, + * getNextPageParam: (lastPage) => lastPage.nextId, + * }) + * + * if (postId == null) return 'Select a post' + * if (isLoading) return 'Loading...' + * if (isError) return Error: {error.message} + * + * return ( + *
    + * {data?.pages.map((page) => page.comments.map((c) =>
  • {c.text}
  • ))} + *
* ) * } * ``` diff --git a/packages/preact-query/src/useIsFetching.ts b/packages/preact-query/src/useIsFetching.ts index 9deac24f6c..16c00bbf95 100644 --- a/packages/preact-query/src/useIsFetching.ts +++ b/packages/preact-query/src/useIsFetching.ts @@ -19,10 +19,12 @@ import { useSyncExternalStore } from './utils' * ```tsx * import { useIsFetching } from '@tanstack/preact-query' * - * // How many queries are fetching? - * const isFetching = useIsFetching() - * // How many queries matching the posts prefix are fetching? - * const isFetchingPosts = useIsFetching({ queryKey: ['posts'] }) + * function PostsFetchingIndicator() { + * // How many queries matching the posts prefix are fetching? + * const isFetchingPosts = useIsFetching({ queryKey: ['posts'] }) + * + * return isFetchingPosts ? Refreshing posts... : null + * } * ``` * * @example diff --git a/packages/preact-query/src/useMutation.ts b/packages/preact-query/src/useMutation.ts index 68a3759d93..eb3565732f 100644 --- a/packages/preact-query/src/useMutation.ts +++ b/packages/preact-query/src/useMutation.ts @@ -21,7 +21,8 @@ import { useSyncExternalStore } from './utils' * Unlike queries, mutations are typically used to create/update/delete data or perform server side-effects. * `useMutation` is the hook for that. * - * @see {@link mutationOptions} to share these options across multiple `useMutation` call sites. + * @see {@link mutationOptions} to share these options across multiple `useMutation` call sites, or to look + * the mutation up elsewhere via its `mutationKey` (e.g. with `useMutationState`). * @param options - The {@link UseMutationOptions} to use — everything you can pass to `useMutation`. * @param queryClient - Use this to use a custom `QueryClient`. Otherwise, the one from the nearest context will * be used. @@ -168,13 +169,13 @@ import { useSyncExternalStore } from './utils' * }) * * async function handleAddAll(todos: Array) { - * const results = await Promise.allSettled( + * const addResults = await Promise.allSettled( * todos.map((todo) => addMutation.mutateAsync(todo)), * ) * - * results.forEach((result, index) => { - * if (result.status === 'rejected') { - * console.error(`Failed to add "${todos[index]}":`, result.reason) + * addResults.forEach((addResult, index) => { + * if (addResult.status === 'rejected') { + * console.error(`Failed to add "${todos[index]}":`, addResult.reason) * } * }) * } diff --git a/packages/preact-query/src/useMutationState.ts b/packages/preact-query/src/useMutationState.ts index beb5318217..58e154a394 100644 --- a/packages/preact-query/src/useMutationState.ts +++ b/packages/preact-query/src/useMutationState.ts @@ -24,10 +24,12 @@ import { useSyncExternalStore } from './utils' * ```tsx * import { useIsMutating } from '@tanstack/preact-query' * - * // How many mutations are fetching? - * const isMutating = useIsMutating() - * // How many mutations matching the posts prefix are fetching? - * const isMutatingPosts = useIsMutating({ mutationKey: ['posts'] }) + * function PostsMutatingIndicator() { + * // How many mutations matching the posts prefix are in progress? + * const isMutatingPosts = useIsMutating({ mutationKey: ['posts'] }) + * + * return isMutatingPosts ? Saving posts... : null + * } * ``` */ export function useIsMutating( @@ -95,10 +97,14 @@ function getResult< * ```tsx * import { useMutationState } from '@tanstack/preact-query' * - * const variables = useMutationState({ - * filters: { status: 'pending' }, - * select: (mutation) => mutation.state.variables, - * }) + * function PendingPosts() { + * const pendingVariables = useMutationState({ + * filters: { status: 'pending' }, + * select: (mutation) => mutation.state.variables, + * }) + * + * return <>{pendingVariables.length} posts saving... + * } * ``` * * @example @@ -108,17 +114,25 @@ function getResult< * * const mutationKey = ['posts'] * - * // Some mutation that we want to get the state for - * const mutation = useMutation({ - * mutationKey, - * mutationFn: createPosts, - * }) - * - * const data = useMutationState({ - * // this mutation key needs to match the mutation key of the given mutation (see above) - * filters: { mutationKey }, - * select: (mutation) => mutation.state.data, - * }) + * function Posts() { + * // Some mutation that we want to get the state for + * const mutation = useMutation({ + * mutationKey, + * mutationFn: createPosts, + * }) + * + * const savedPosts = useMutationState({ + * // this mutation key needs to match the mutation key of the given mutation (see above) + * filters: { mutationKey, status: 'success' }, + * select: (mutation) => mutation.state.data, + * }) + * + * return ( + * + * ) + * } * ``` * * @example @@ -126,12 +140,18 @@ function getResult< * mutation cache for `gcTime` milliseconds — check the last item that `useMutationState` returns to get the * latest invocation: * ```tsx - * const data = useMutationState({ - * filters: { mutationKey: ['posts'] }, - * select: (mutation) => mutation.state.data, - * }) + * import { useMutationState } from '@tanstack/preact-query' + * + * function LatestPost() { + * const savedPosts = useMutationState({ + * filters: { mutationKey: ['posts'], status: 'success' }, + * select: (mutation) => mutation.state.data, + * }) + * + * const latestSavedPost = savedPosts[savedPosts.length - 1] * - * const latest = data[data.length - 1] + * return <>{latestSavedPost ? 'Saved' : 'Nothing saved yet'} + * } * ``` */ export function useMutationState< diff --git a/packages/preact-query/src/useQueries.ts b/packages/preact-query/src/useQueries.ts index c47e5a91b3..a49c29df87 100644 --- a/packages/preact-query/src/useQueries.ts +++ b/packages/preact-query/src/useQueries.ts @@ -244,32 +244,58 @@ export type QueriesResults< * ```tsx * import { useQueries } from '@tanstack/preact-query' * - * const ids = [1, 2, 3] - * const results = useQueries({ - * queries: ids.map((id) => ({ - * queryKey: ['post', id], - * queryFn: () => fetchPost(id), - * staleTime: Infinity, - * })), - * }) + * function Posts({ ids }: { ids: Array }) { + * const postQueries = useQueries({ + * queries: ids.map((id) => ({ + * queryKey: ['post', id], + * queryFn: () => fetchPost(id), + * staleTime: Infinity, + * })), + * }) + * + * return ( + *
    + * {postQueries.map((query, index) => { + * if (query.isPending) return
  • Loading...
  • + * if (query.isError) return
  • Error: {query.error.message}
  • + * return
  • {query.data.title}
  • + * })} + *
+ * ) + * } * ``` * * @example * Combining results into a single value: * ```tsx - * const ids = [1, 2, 3] - * const combinedQueries = useQueries({ - * queries: ids.map((id) => ({ - * queryKey: ['post', id], - * queryFn: () => fetchPost(id), - * })), - * combine: (results) => { - * return { - * data: results.map((result) => result.data), - * pending: results.some((result) => result.isPending), - * } - * }, - * }) + * import { useQueries } from '@tanstack/preact-query' + * + * function Posts({ ids }: { ids: Array }) { + * const { data, isPending, isError } = useQueries({ + * queries: ids.map((id) => ({ + * queryKey: ['post', id], + * queryFn: () => fetchPost(id), + * })), + * combine: (postQueries) => { + * return { + * data: postQueries.map((query) => query.data), + * isPending: postQueries.some((query) => query.isPending), + * isError: postQueries.some((query) => query.isError), + * } + * }, + * }) + * + * if (isPending) return 'Loading...' + * if (isError) return 'Error loading posts' + * + * return ( + *
    + * {data.map((post) => ( + *
  • {post?.title}
  • + * ))} + *
+ * ) + * } * ``` */ export function useQueries< diff --git a/packages/preact-query/src/useQuery.ts b/packages/preact-query/src/useQuery.ts index 0f417095aa..e005fe2f54 100644 --- a/packages/preact-query/src/useQuery.ts +++ b/packages/preact-query/src/useQuery.ts @@ -28,14 +28,22 @@ import { useBaseQuery } from './useBaseQuery' * import { useQuery } from '@tanstack/preact-query' * * function Posts() { - * // `data` is `Post[]`, never `undefined`, thanks to `initialData`. - * const { data } = useQuery({ + * // `data` is `Post[]`, never `undefined`, thanks to `initialData` — even if a refetch fails, + * // so the list stays visible alongside the error. + * const { data, isError, error } = useQuery({ * queryKey: ['posts'], * queryFn: fetchPosts, * initialData: [], * }) * - * return <>{data.map((post) =>

{post.title}

)} + * return ( + *
+ * {isError ? Error: {error.message} : null} + *
    + * {data.map((post) =>
  • {post.title}
  • )} + *
+ *
+ * ) * } * ``` */ @@ -75,9 +83,11 @@ export function useQuery< * * return ( *
- * {data.map((post) => ( - *

{post.title}

- * ))} + *
    + * {data.map((post) => ( + *
  • {post.title}
  • + * ))} + *
*
{isFetching ? 'Background Updating...' : ' '}
*
* ) @@ -98,7 +108,11 @@ export function useQuery< * if (isPending) return 'Loading...' * if (isError) return Error: {error.message} * - * return <>{data.map((post) =>

{post.title}

)} + * return ( + *
    + * {data.map((post) =>
  • {post.title}
  • )} + *
+ * ) * } * ``` */ @@ -138,9 +152,11 @@ export function useQuery< * * return ( *
- * {data.map((post) => ( - *

{post.title}

- * ))} + *
    + * {data.map((post) => ( + *
  • {post.title}
  • + * ))} + *
*
{isFetching ? 'Background Updating...' : ' '}
*
* ) @@ -169,6 +185,28 @@ export function useQuery< * ``` * * @example + * The same dependent query, type safe: `skipToken` disables the query without needing the + * non-null assertion above, since `queryFn` is only ever called when `postId` is defined. + * `refetch` doesn't work while `queryFn` is `skipToken` — use `enabled: false` instead if you + * need to trigger the query manually: + * ```tsx + * import { skipToken, useQuery } from '@tanstack/preact-query' + * + * function Post({ postId }: { postId: number | undefined }) { + * const { data, isLoading, isError, error } = useQuery({ + * queryKey: ['post', postId], + * queryFn: postId != null ? () => fetchPost(postId) : skipToken, + * }) + * + * if (postId == null) return 'Select a post' + * if (isLoading) return 'Loading...' + * if (isError) return Error: {error.message} + * + * return

{data?.title}

+ * } + * ``` + * + * @example * Seeding a detail query from an already-cached list, to skip the loading state: * ```tsx * import { useQuery, useQueryClient } from '@tanstack/preact-query' @@ -176,7 +214,7 @@ export function useQuery< * function Post({ postId }: { postId: number }) { * const queryClient = useQueryClient() * - * const { data } = useQuery({ + * const { data, isError, error } = useQuery({ * queryKey: ['post', postId], * queryFn: () => fetchPost(postId), * initialData: () => @@ -185,6 +223,8 @@ export function useQuery< * ?.find((post) => post.id === postId), * }) * + * if (isError) return Error: {error.message} + * * return

{data?.title}

* } * ``` @@ -198,15 +238,19 @@ export function useQuery< * function Posts() { * const [page, setPage] = useState(0) * - * const { data, isPlaceholderData } = useQuery({ + * const { data, isPlaceholderData, isError, error } = useQuery({ * queryKey: ['posts', page], * queryFn: () => fetchPosts(page), * placeholderData: keepPreviousData, * }) * + * if (isError) return Error: {error.message} + * * return ( *
- * {data?.map((post) =>

{post.title}

)} + *
    + * {data?.map((post) =>
  • {post.title}
  • )} + *
*