Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
156 changes: 156 additions & 0 deletions docs/framework/angular/guides/infinite-queries.md
Original file line number Diff line number Diff line change
@@ -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
<div>
@if (query.isPending()) {
<p>Loading...</p>
} @else if (query.isError()) {
<span>Error: {{ query?.error().message }}</span>
} @else { @for (page of query?.data().pages; track $index) { @for (project of
page.data; track project.id) {
<p>{{ project.name }} {{ project.id }}</p>
} }
<div>
<button (click)="query.fetchNextPage()" [disabled]="nextButtonDisabled()">
{{ nextButtonText() }}
</button>
</div>
}
</div>
```

[//]: # 'Example'
[//]: # 'Example1'

```ts
@Component({
template: ` <list-component (endReached)="fetchNextPage()" /> `,
})
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'
4 changes: 2 additions & 2 deletions docs/framework/react/guides/infinite-queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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',
})
})
})
12 changes: 12 additions & 0 deletions packages/angular-query-experimental/src/__tests__/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,18 @@ export function rejectFetcher(): Promise<Error> {
})
}

export function infiniteFetcher({
pageParam,
}: {
pageParam?: number
}): Promise<string> {
return new Promise((resolve) => {
setTimeout(() => {
return resolve('data on page ' + pageParam)
}, 0)
})
}

export function successMutator<T>(param: T): Promise<T> {
return new Promise((resolve) => {
setTimeout(() => {
Expand Down
1 change: 1 addition & 0 deletions packages/angular-query-experimental/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
76 changes: 71 additions & 5 deletions packages/angular-query-experimental/src/infinite-query-options.ts
Original file line number Diff line number Diff line change
@@ -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<TQueryFnData>,
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown,
> = CreateInfiniteQueryOptions<
TQueryFnData,
TError,
TData,
TQueryFnData,
TQueryKey,
TPageParam
> & {
initialData?: undefined
}

type NonUndefinedGuard<T> = T extends undefined ? never : T

export type DefinedInitialDataInfiniteOptions<
TQueryFnData,
TError = DefaultError,
TData = InfiniteData<TQueryFnData>,
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown,
> = CreateInfiniteQueryOptions<
TQueryFnData,
TError,
TData,
TQueryFnData,
TQueryKey,
TPageParam
> & {
initialData:
| NonUndefinedGuard<InfiniteData<TQueryFnData, TPageParam>>
| (() => NonUndefinedGuard<InfiniteData<TQueryFnData, TPageParam>>)
}

export function infiniteQueryOptions<
TQueryFnData,
Expand All @@ -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<TQueryKey, InfiniteData<TQueryFnData>>
}

export function infiniteQueryOptions<
TQueryFnData,
TError = DefaultError,
TData = InfiniteData<TQueryFnData>,
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown,
>(
options: DefinedInitialDataInfiniteOptions<
TQueryFnData,
TError,
TData,
TQueryKey,
TPageParam
>,
): DefinedInitialDataInfiniteOptions<
TQueryFnData,
TError,
TData,
TQueryKey,
TPageParam
> {
> & {
queryKey: DataTag<TQueryKey, InfiniteData<TQueryFnData>>
}

export function infiniteQueryOptions(options: unknown) {
return options
}
Loading