diff --git a/docs/framework/angular/guides/infinite-queries.md b/docs/framework/angular/guides/infinite-queries.md
new file mode 100644
index 0000000000..d299142600
--- /dev/null
+++ b/docs/framework/angular/guides/infinite-queries.md
@@ -0,0 +1,156 @@
+---
+id: infinite-queries
+title: Infinite Queries
+ref: docs/framework/react/guides/infinite-queries.md
+replace:
+ { 'useQuery': 'injectQuery', 'useInfiniteQuery': 'injectInfiniteQuery' }
+---
+
+[//]: # 'Example'
+
+```ts
+import { Component, computed, inject } from '@angular/core'
+import { injectInfiniteQuery } from '@tanstack/angular-query-experimental'
+import { lastValueFrom } from 'rxjs'
+import { ProjectsService } from './projects-service'
+
+@Component({
+ selector: 'example',
+ templateUrl: './example.component.html',
+})
+export class Example {
+ projectsService = inject(ProjectsService)
+
+ query = injectInfiniteQuery(() => ({
+ queryKey: ['projects'],
+ queryFn: async ({ pageParam }) => {
+ return lastValueFrom(this.projectsService.getProjects(pageParam))
+ },
+ initialPageParam: 0,
+ getPreviousPageParam: (firstPage) => firstPage.previousId ?? undefined,
+ getNextPageParam: (lastPage) => lastPage.nextId ?? undefined,
+ maxPages: 3,
+ }))
+
+ nextButtonDisabled = computed(
+ () => !this.#hasNextPage() || this.#isFetchingNextPage(),
+ )
+ nextButtonText = computed(() =>
+ this.#isFetchingNextPage()
+ ? 'Loading more...'
+ : this.#hasNextPage()
+ ? 'Load newer'
+ : 'Nothing more to load',
+ )
+
+ #hasNextPage = this.query.hasNextPage
+ #isFetchingNextPage = this.query.isFetchingNextPage
+}
+```
+
+```html
+
+ @if (query.isPending()) {
+
Loading...
+ } @else if (query.isError()) {
+
Error: {{ query?.error().message }}
+ } @else { @for (page of query?.data().pages; track $index) { @for (project of
+ page.data; track project.id) {
+
{{ project.name }} {{ project.id }}
+ } }
+
+
+
+ }
+
+```
+
+[//]: # 'Example'
+[//]: # 'Example1'
+
+```ts
+@Component({
+ template: ` `,
+})
+export class Example {
+ query = injectInfiniteQuery(() => ({
+ queryKey: ['projects'],
+ queryFn: async ({ pageParam }) => {
+ return lastValueFrom(this.projectsService.getProjects(pageParam))
+ },
+ }))
+
+ fetchNextPage() {
+ // Do nothing if already fetching
+ if (this.query.isFetching()) return
+ this.query.fetchNextPage()
+ }
+}
+```
+
+[//]: # 'Example1'
+[//]: # 'Example3'
+
+```ts
+query = injectInfiniteQuery(() => ({
+ queryKey: ['projects'],
+ queryFn: fetchProjects,
+ getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
+ getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,
+}))
+```
+
+[//]: # 'Example3'
+[//]: # 'Example4'
+
+```ts
+query = injectInfiniteQuery(() => ({
+ queryKey: ['projects'],
+ queryFn: fetchProjects,
+ select: (data) => ({
+ pages: [...data.pages].reverse(),
+ pageParams: [...data.pageParams].reverse(),
+ }),
+}))
+```
+
+[//]: # 'Example4'
+[//]: # 'Example8'
+
+```ts
+injectInfiniteQuery(() => ({
+ queryKey: ['projects'],
+ queryFn: fetchProjects,
+ initialPageParam: 0,
+ getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
+ getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,
+ maxPages: 3,
+}))
+```
+
+[//]: # 'Example8'
+[//]: # 'Example9'
+
+```ts
+injectInfiniteQuery(() => ({
+ queryKey: ['projects'],
+ queryFn: fetchProjects,
+ initialPageParam: 0,
+ getNextPageParam: (lastPage, allPages, lastPageParam) => {
+ if (lastPage.length === 0) {
+ return undefined
+ }
+ return lastPageParam + 1
+ },
+ getPreviousPageParam: (firstPage, allPages, firstPageParam) => {
+ if (firstPageParam <= 1) {
+ return undefined
+ }
+ return firstPageParam - 1
+ },
+}))
+```
+
+[//]: # 'Example9'
diff --git a/docs/framework/react/guides/infinite-queries.md b/docs/framework/react/guides/infinite-queries.md
index d842293536..d3c003405c 100644
--- a/docs/framework/react/guides/infinite-queries.md
+++ b/docs/framework/react/guides/infinite-queries.md
@@ -204,8 +204,6 @@ queryClient.setQueryData(['projects'], (data) => ({
Make sure to always keep the same data structure of pages and pageParams!
-[//]: # 'Example8'
-
## What if I want to limit the number of pages?
In some use cases you may want to limit the number of pages stored in the query data to improve the performance and UX:
@@ -230,6 +228,8 @@ useInfiniteQuery({
})
```
+[//]: # 'Example8'
+
## What if my API doesn't return a cursor?
If your API doesn't return a cursor, you can use the `pageParam` as a cursor. Because `getNextPageParam` and `getPreviousPageParam` also get the `pageParam`of the current page, you can use it to calculate the next / previous page param.
diff --git a/packages/angular-query-experimental/src/__tests__/inject-infinite-query.test.ts b/packages/angular-query-experimental/src/__tests__/inject-infinite-query.test.ts
new file mode 100644
index 0000000000..f91be7cf7c
--- /dev/null
+++ b/packages/angular-query-experimental/src/__tests__/inject-infinite-query.test.ts
@@ -0,0 +1,64 @@
+import { TestBed } from '@angular/core/testing'
+import { QueryClient } from '@tanstack/query-core'
+import { afterEach } from 'vitest'
+import { injectInfiniteQuery } from '../inject-infinite-query'
+import { provideAngularQuery } from '../providers'
+import { expectSignals, infiniteFetcher } from './test-utils'
+
+const QUERY_DURATION = 1000
+
+const resolveQueries = () => vi.advanceTimersByTimeAsync(QUERY_DURATION)
+
+describe('injectInfiniteQuery', () => {
+ let queryClient: QueryClient
+
+ beforeEach(() => {
+ queryClient = new QueryClient()
+ vi.useFakeTimers()
+ TestBed.configureTestingModule({
+ providers: [provideAngularQuery(queryClient)],
+ })
+ })
+
+ afterEach(() => {
+ vi.useRealTimers()
+ })
+
+ test('should properly execute infinite query', async () => {
+ const query = TestBed.runInInjectionContext(() => {
+ return injectInfiniteQuery(() => ({
+ queryKey: ['infiniteQuery'],
+ queryFn: infiniteFetcher,
+ initialPageParam: 0,
+ getNextPageParam: () => 12,
+ }))
+ })
+
+ expectSignals(query, {
+ data: undefined,
+ status: 'pending',
+ })
+
+ await resolveQueries()
+
+ expectSignals(query, {
+ data: {
+ pageParams: [0],
+ pages: ['data on page 0'],
+ },
+ status: 'success',
+ })
+
+ void query.fetchNextPage()
+
+ await resolveQueries()
+
+ expectSignals(query, {
+ data: {
+ pageParams: [0, 12],
+ pages: ['data on page 0', 'data on page 12'],
+ },
+ status: 'success',
+ })
+ })
+})
diff --git a/packages/angular-query-experimental/src/__tests__/test-utils.ts b/packages/angular-query-experimental/src/__tests__/test-utils.ts
index 1b3055c955..fd6ae3348a 100644
--- a/packages/angular-query-experimental/src/__tests__/test-utils.ts
+++ b/packages/angular-query-experimental/src/__tests__/test-utils.ts
@@ -30,6 +30,18 @@ export function rejectFetcher(): Promise {
})
}
+export function infiniteFetcher({
+ pageParam,
+}: {
+ pageParam?: number
+}): Promise {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ return resolve('data on page ' + pageParam)
+ }, 0)
+ })
+}
+
export function successMutator(param: T): Promise {
return new Promise((resolve) => {
setTimeout(() => {
diff --git a/packages/angular-query-experimental/src/index.ts b/packages/angular-query-experimental/src/index.ts
index 13c98b432e..89587b496d 100644
--- a/packages/angular-query-experimental/src/index.ts
+++ b/packages/angular-query-experimental/src/index.ts
@@ -13,6 +13,7 @@ export { queryOptions } from './query-options'
export { infiniteQueryOptions } from './infinite-query-options'
+export * from './inject-infinite-query'
export * from './inject-is-fetching'
export * from './inject-is-mutating'
export * from './inject-mutation'
diff --git a/packages/angular-query-experimental/src/infinite-query-options.ts b/packages/angular-query-experimental/src/infinite-query-options.ts
index a61302af03..dae1cdd848 100644
--- a/packages/angular-query-experimental/src/infinite-query-options.ts
+++ b/packages/angular-query-experimental/src/infinite-query-options.ts
@@ -1,5 +1,45 @@
-import type { DefaultError, InfiniteData, QueryKey } from '@tanstack/query-core'
+import type { DataTag } from '@tanstack/query-core'
+import type { InfiniteData } from '@tanstack/query-core'
import type { CreateInfiniteQueryOptions } from './types'
+import type { DefaultError, QueryKey } from '@tanstack/query-core'
+
+export type UndefinedInitialDataInfiniteOptions<
+ TQueryFnData,
+ TError = DefaultError,
+ TData = InfiniteData,
+ TQueryKey extends QueryKey = QueryKey,
+ TPageParam = unknown,
+> = CreateInfiniteQueryOptions<
+ TQueryFnData,
+ TError,
+ TData,
+ TQueryFnData,
+ TQueryKey,
+ TPageParam
+> & {
+ initialData?: undefined
+}
+
+type NonUndefinedGuard = T extends undefined ? never : T
+
+export type DefinedInitialDataInfiniteOptions<
+ TQueryFnData,
+ TError = DefaultError,
+ TData = InfiniteData,
+ TQueryKey extends QueryKey = QueryKey,
+ TPageParam = unknown,
+> = CreateInfiniteQueryOptions<
+ TQueryFnData,
+ TError,
+ TData,
+ TQueryFnData,
+ TQueryKey,
+ TPageParam
+> & {
+ initialData:
+ | NonUndefinedGuard>
+ | (() => NonUndefinedGuard>)
+}
export function infiniteQueryOptions<
TQueryFnData,
@@ -8,21 +48,47 @@ export function infiniteQueryOptions<
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown,
>(
- options: CreateInfiniteQueryOptions<
+ options: UndefinedInitialDataInfiniteOptions<
TQueryFnData,
TError,
TData,
- TQueryFnData,
TQueryKey,
TPageParam
>,
-): CreateInfiniteQueryOptions<
+): UndefinedInitialDataInfiniteOptions<
TQueryFnData,
TError,
TData,
+ TQueryKey,
+ TPageParam
+> & {
+ queryKey: DataTag>
+}
+
+export function infiniteQueryOptions<
+ TQueryFnData,
+ TError = DefaultError,
+ TData = InfiniteData,
+ TQueryKey extends QueryKey = QueryKey,
+ TPageParam = unknown,
+>(
+ options: DefinedInitialDataInfiniteOptions<
+ TQueryFnData,
+ TError,
+ TData,
+ TQueryKey,
+ TPageParam
+ >,
+): DefinedInitialDataInfiniteOptions<
TQueryFnData,
+ TError,
+ TData,
TQueryKey,
TPageParam
-> {
+> & {
+ queryKey: DataTag>
+}
+
+export function infiniteQueryOptions(options: unknown) {
return options
}
diff --git a/packages/angular-query-experimental/src/inject-infinite-query.ts b/packages/angular-query-experimental/src/inject-infinite-query.ts
new file mode 100644
index 0000000000..42a0f64381
--- /dev/null
+++ b/packages/angular-query-experimental/src/inject-infinite-query.ts
@@ -0,0 +1,93 @@
+import { InfiniteQueryObserver } from '@tanstack/query-core'
+import { createBaseQuery } from './create-base-query'
+import { injectQueryClient } from './inject-query-client'
+import { assertInjector } from './util/assert-injector/assert-injector'
+import type { Injector } from '@angular/core'
+import type {
+ DefaultError,
+ InfiniteData,
+ QueryClient,
+ QueryKey,
+ QueryObserver,
+} from '@tanstack/query-core'
+import type {
+ CreateInfiniteQueryOptions,
+ CreateInfiniteQueryResult,
+ DefinedCreateInfiniteQueryResult,
+} from './types'
+import type {
+ DefinedInitialDataInfiniteOptions,
+ UndefinedInitialDataInfiniteOptions,
+} from './infinite-query-options'
+
+export function injectInfiniteQuery<
+ TQueryFnData,
+ TError = DefaultError,
+ TData = InfiniteData,
+ TQueryKey extends QueryKey = QueryKey,
+ TPageParam = unknown,
+>(
+ options: (
+ client: QueryClient,
+ ) => UndefinedInitialDataInfiniteOptions<
+ TQueryFnData,
+ TError,
+ TData,
+ TQueryKey,
+ TPageParam
+ >,
+ injector?: Injector,
+): CreateInfiniteQueryResult
+
+export function injectInfiniteQuery<
+ TQueryFnData,
+ TError = DefaultError,
+ TData = InfiniteData,
+ TQueryKey extends QueryKey = QueryKey,
+ TPageParam = unknown,
+>(
+ options: (
+ client: QueryClient,
+ ) => DefinedInitialDataInfiniteOptions<
+ TQueryFnData,
+ TError,
+ TData,
+ TQueryKey,
+ TPageParam
+ >,
+ injector?: Injector,
+): DefinedCreateInfiniteQueryResult
+
+export function injectInfiniteQuery<
+ TQueryFnData,
+ TError = DefaultError,
+ TData = InfiniteData,
+ TQueryKey extends QueryKey = QueryKey,
+ TPageParam = unknown,
+>(
+ options: (
+ client: QueryClient,
+ ) => CreateInfiniteQueryOptions<
+ TQueryFnData,
+ TError,
+ TData,
+ TQueryFnData,
+ TQueryKey,
+ TPageParam
+ >,
+ injector?: Injector,
+): CreateInfiniteQueryResult
+
+export function injectInfiniteQuery(
+ options: (client: QueryClient) => CreateInfiniteQueryOptions,
+ injector?: Injector,
+) {
+ return assertInjector(injectInfiniteQuery, injector, () => {
+ const queryClient = injectQueryClient()
+ return createBaseQuery(
+ options,
+ InfiniteQueryObserver as typeof QueryObserver,
+ queryClient,
+ )
+ })
+}
diff --git a/packages/angular-query-experimental/src/types.ts b/packages/angular-query-experimental/src/types.ts
index bdc047e109..a8bc7ef46c 100644
--- a/packages/angular-query-experimental/src/types.ts
+++ b/packages/angular-query-experimental/src/types.ts
@@ -1,7 +1,8 @@
-import type { Signal } from '@angular/core'
+/* istanbul ignore file */
import type {
DefaultError,
+ DefinedInfiniteQueryObserverResult,
DefinedQueryObserverResult,
InfiniteQueryObserverOptions,
InfiniteQueryObserverResult,
@@ -26,6 +27,25 @@ export interface CreateBaseQueryOptions<
'queryKey'
> {}
+export interface CreateQueryOptions<
+ TQueryFnData = unknown,
+ TError = DefaultError,
+ TData = TQueryFnData,
+ TQueryKey extends QueryKey = QueryKey,
+> extends Omit<
+ WithRequired<
+ CreateBaseQueryOptions<
+ TQueryFnData,
+ TError,
+ TData,
+ TQueryFnData,
+ TQueryKey
+ >,
+ 'queryKey'
+ >,
+ 'suspense'
+ > {}
+
type CreateStatusBasedQueryResult<
TStatus extends QueryObserverResult['status'],
TData = unknown,
@@ -56,74 +76,69 @@ export interface BaseQueryNarrowing {
>
}
-export type CreateBaseQueryResult<
- TData = unknown,
- TError = DefaultError,
- TState = QueryObserverResult,
-> = BaseQueryNarrowing &
- MapToSignals>
-
-export interface CreateQueryOptions<
+export interface CreateInfiniteQueryOptions<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
+ TQueryData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
-> extends Omit<
- WithRequired<
- CreateBaseQueryOptions<
+ TPageParam = unknown,
+> extends WithRequired<
+ Omit<
+ InfiniteQueryObserverOptions<
TQueryFnData,
TError,
TData,
- TQueryFnData,
- TQueryKey
+ TQueryData,
+ TQueryKey,
+ TPageParam
>,
- 'queryKey'
+ 'suspense'
>,
- 'suspense'
+ 'queryKey'
> {}
+export type CreateBaseQueryResult<
+ TData = unknown,
+ TError = DefaultError,
+ TState = QueryObserverResult,
+> = BaseQueryNarrowing &
+ MapToSignals>
+
export type CreateQueryResult<
TData = unknown,
TError = DefaultError,
> = CreateBaseQueryResult
-/** Options for createInfiniteQuery */
-export type CreateInfiniteQueryOptions<
- TQueryFnData = unknown,
+export type DefinedCreateQueryResult<
+ TData = unknown,
TError = DefaultError,
- TData = TQueryFnData,
- TQueryData = TQueryFnData,
- TQueryKey extends QueryKey = QueryKey,
- TPageParam = unknown,
-> = InfiniteQueryObserverOptions<
- TQueryFnData,
- TError,
- TData,
- TQueryData,
- TQueryKey,
- TPageParam
->
+ TDefinedQueryObserver = DefinedQueryObserverResult,
+> = MapToSignals
export type CreateInfiniteQueryResult<
TData = unknown,
TError = DefaultError,
-> = Signal>
+> = MapToSignals>
-export type DefinedCreateQueryResult<
+export type DefinedCreateInfiniteQueryResult<
TData = unknown,
TError = DefaultError,
- TDefinedQueryObserver = DefinedQueryObserverResult,
-> = MapToSignals
+ TDefinedInfiniteQueryObserver = DefinedInfiniteQueryObserverResult<
+ TData,
+ TError
+ >,
+> = MapToSignals
-export type CreateMutationOptions<
+export interface CreateMutationOptions<
TData = unknown,
TError = DefaultError,
TVariables = void,
TContext = unknown,
-> = Omit<
- MutationObserverOptions,
- '_defaulted' | 'variables'
->
+> extends Omit<
+ MutationObserverOptions,
+ '_defaulted' | 'variables'
+ > {}
export type CreateMutateFunction<
TData = unknown,
@@ -226,7 +241,6 @@ export interface BaseMutationNarrowing<
>
}
-/** Result from createMutation */
export type CreateMutationResult<
TData = unknown,
TError = DefaultError,