snapshot of https://github.com/TanStack/query.git (main, 2026-09-16; blobs > 131072B omitted)
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
---
|
||||
id: background-fetching-indicators
|
||||
title: Background Fetching Indicators
|
||||
ref: docs/framework/react/guides/background-fetching-indicators.md
|
||||
replace:
|
||||
{
|
||||
'useIsFetching': 'injectIsFetching',
|
||||
'hook': 'function',
|
||||
'@tanstack/react-query': '@tanstack/angular-query-experimental',
|
||||
}
|
||||
---
|
||||
|
||||
[//]: # 'Example'
|
||||
|
||||
```angular-ts
|
||||
@Component({
|
||||
selector: 'todos',
|
||||
template: `
|
||||
@if (todosQuery.isPending()) {
|
||||
Loading...
|
||||
} @else if (todosQuery.isError()) {
|
||||
An error has occurred: {{ todosQuery.error().message }}
|
||||
} @else if (todosQuery.isSuccess()) {
|
||||
@if (todosQuery.isFetching()) {
|
||||
Refreshing...
|
||||
}
|
||||
@for (todos of todosQuery.data(); track todo.id) {
|
||||
<todo [todo]="todo" />
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
class TodosComponent {
|
||||
readonly todosQuery = injectQuery(() => ({
|
||||
queryKey: ['todos'],
|
||||
queryFn: fetchTodos,
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
[//]: # 'Example'
|
||||
[//]: # 'Example2'
|
||||
|
||||
```angular-ts
|
||||
import { injectIsFetching } from '@tanstack/angular-query-experimental'
|
||||
|
||||
@Component({
|
||||
selector: 'global-loading-indicator',
|
||||
template: `
|
||||
@if (isFetching()) {
|
||||
<div>Queries are fetching in the background...</div>
|
||||
}
|
||||
`,
|
||||
})
|
||||
export class GlobalLoadingIndicatorComponent {
|
||||
readonly isFetching = injectIsFetching()
|
||||
}
|
||||
```
|
||||
|
||||
[//]: # 'Example2'
|
||||
35
docs/framework/angular/guides/caching.md
Normal file
35
docs/framework/angular/guides/caching.md
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
id: caching
|
||||
title: Caching Examples
|
||||
---
|
||||
|
||||
> Please thoroughly read the [Important Defaults](./important-defaults.md) before reading this guide
|
||||
|
||||
## Basic Example
|
||||
|
||||
This caching example illustrates the story and lifecycle of:
|
||||
|
||||
- Query Instances with and without cache data
|
||||
- Background Refetching
|
||||
- Inactive Queries
|
||||
- Garbage Collection
|
||||
|
||||
Let's assume we are using the default `gcTime` of **5 minutes** and the default `staleTime` of `0`.
|
||||
|
||||
- A new instance of `injectQuery(() => ({ queryKey: ['todos'], queryFn: fetchTodos }))` initializes.
|
||||
- Since no other queries have been made with the `['todos']` query key, this query will show a hard loading state and make a network request to fetch the data.
|
||||
- When the network request has completed, the returned data will be cached under the `['todos']` key.
|
||||
- The data will be marked as stale after the configured `staleTime` (defaults to `0`, or immediately).
|
||||
- A second instance of `injectQuery(() => ({ queryKey: ['todos'], queryFn: fetchTodos }))` initializes elsewhere.
|
||||
- Since the cache already has data for the `['todos']` key from the first query, that data is immediately returned from the cache.
|
||||
- The new instance triggers a new network request using its query function.
|
||||
- Note that regardless of whether both `fetchTodos` query functions are identical or not, both queries' [`status`](../reference/functions/injectQuery.md) are updated (including `isFetching`, `isPending`, and other related values) because they have the same query key.
|
||||
- When the request completes successfully, the cache's data under the `['todos']` key is updated with the new data, and both instances are updated with the new data.
|
||||
- Both instances of the `injectQuery(() => ({ queryKey: ['todos'], queryFn: fetchTodos }))` query are destroyed and no longer in use.
|
||||
- Since there are no more active instances of this query, a garbage collection timeout is set using `gcTime` to delete and garbage collect the query (defaults to **5 minutes**).
|
||||
- Before the cache timeout has completed, another instance of `injectQuery(() => ({ queryKey: ['todos'], queryFn: fetchTodos }))` mounts. The query immediately returns the available cached data while the `fetchTodos` function is being run in the background. When it completes successfully, it will populate the cache with fresh data.
|
||||
- The final instance of `injectQuery(() => ({ queryKey: ['todos'], queryFn: fetchTodos }))` gets destroyed.
|
||||
- No more instances of `injectQuery(() => ({ queryKey: ['todos'], queryFn: fetchTodos }))` appear within **5 minutes**.
|
||||
- The cached data under the `['todos']` key is deleted and garbage collected.
|
||||
|
||||
For more advanced use-cases, see [injectQuery](../reference/functions/injectQuery.md).
|
||||
49
docs/framework/angular/guides/default-query-function.md
Normal file
49
docs/framework/angular/guides/default-query-function.md
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
id: default-query-function
|
||||
title: Default Query Function
|
||||
ref: docs/framework/react/guides/default-query-function.md
|
||||
---
|
||||
|
||||
[//]: # 'Example'
|
||||
|
||||
```ts
|
||||
// Define a default query function that will receive the query key
|
||||
const defaultQueryFn: QueryFunction = async ({ queryKey }) => {
|
||||
const { data } = await axios.get(
|
||||
`https://jsonplaceholder.typicode.com${queryKey[0]}`,
|
||||
)
|
||||
return data
|
||||
}
|
||||
|
||||
// provide the default query function to your app with defaultOptions
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
queryFn: defaultQueryFn,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
bootstrapApplication(MyAppComponent, {
|
||||
providers: [provideTanStackQuery(queryClient)],
|
||||
})
|
||||
|
||||
export class PostsComponent {
|
||||
// All you have to do now is pass a key!
|
||||
readonly postsQuery = injectQuery<Array<Post>>(() => ({
|
||||
queryKey: ['/posts'],
|
||||
}))
|
||||
// ...
|
||||
}
|
||||
|
||||
export class PostComponent {
|
||||
// You can even leave out the queryFn and just go straight into options
|
||||
readonly postQuery = injectQuery<Post>(() => ({
|
||||
enabled: this.postIdSignal() > 0,
|
||||
queryKey: [`/posts/${this.postIdSignal()}`],
|
||||
}))
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
[//]: # 'Example'
|
||||
33
docs/framework/angular/guides/dependent-queries.md
Normal file
33
docs/framework/angular/guides/dependent-queries.md
Normal file
@@ -0,0 +1,33 @@
|
||||
---
|
||||
id: dependent-queries
|
||||
title: Dependent Queries
|
||||
ref: docs/framework/react/guides/dependent-queries.md
|
||||
replace: { 'useQuery': 'injectQuery', 'useQueries': 'injectQueries' }
|
||||
---
|
||||
|
||||
[//]: # 'Example'
|
||||
|
||||
```ts
|
||||
// Get the user
|
||||
userQuery = injectQuery(() => ({
|
||||
queryKey: ['user', email],
|
||||
queryFn: getUserByEmail,
|
||||
}))
|
||||
|
||||
// Then get the user's projects
|
||||
projectsQuery = injectQuery(() => ({
|
||||
queryKey: ['projects', this.userQuery.data()?.id],
|
||||
queryFn: getProjectsByUser,
|
||||
// The query will not execute until the user id exists
|
||||
enabled: !!this.userQuery.data()?.id,
|
||||
}))
|
||||
```
|
||||
|
||||
[//]: # 'Example'
|
||||
[//]: # 'Example2'
|
||||
|
||||
```ts
|
||||
// injectQueries is under development for Angular Query
|
||||
```
|
||||
|
||||
[//]: # 'Example2'
|
||||
95
docs/framework/angular/guides/disabling-queries.md
Normal file
95
docs/framework/angular/guides/disabling-queries.md
Normal file
@@ -0,0 +1,95 @@
|
||||
---
|
||||
id: disabling-queries
|
||||
title: Disabling/Pausing Queries
|
||||
ref: docs/framework/react/guides/disabling-queries.md
|
||||
replace: { 'useQuery': 'injectQuery' }
|
||||
---
|
||||
|
||||
[//]: # 'Example'
|
||||
|
||||
```angular-ts
|
||||
@Component({
|
||||
selector: 'todos',
|
||||
template: `<div>
|
||||
<button (click)="todosQuery.refetch()">Fetch Todos</button>
|
||||
|
||||
@if (todosQuery.data()) {
|
||||
<ul>
|
||||
@for (todo of todosQuery.data(); track todo.id) {
|
||||
<li>{{ todo.title }}</li>
|
||||
}
|
||||
</ul>
|
||||
} @else {
|
||||
@if (todosQuery.isError()) {
|
||||
<span>Error: {{ todosQuery.error().message }}</span>
|
||||
} @else if (todosQuery.isLoading()) {
|
||||
<span>Loading...</span>
|
||||
} @else if (!todosQuery.isLoading() && !todosQuery.isError()) {
|
||||
<span>Not ready ...</span>
|
||||
}
|
||||
}
|
||||
|
||||
<div>{{ todosQuery.isLoading() ? 'Fetching...' : '' }}</div>
|
||||
</div>`,
|
||||
})
|
||||
export class TodosComponent {
|
||||
readonly todosQuery = injectQuery(() => ({
|
||||
queryKey: ['todos'],
|
||||
queryFn: fetchTodoList,
|
||||
enabled: false,
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
[//]: # 'Example'
|
||||
[//]: # 'Example2'
|
||||
|
||||
```angular-ts
|
||||
@Component({
|
||||
selector: 'todos',
|
||||
template: `
|
||||
<div>
|
||||
// 🚀 applying the filter will enable and execute the query
|
||||
<filters-form onApply="filter.set" />
|
||||
<todos-table data="todosQuery.data()" />
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
export class TodosComponent {
|
||||
filter = signal('')
|
||||
|
||||
readonly todosQuery = injectQuery(() => ({
|
||||
queryKey: ['todos', this.filter()],
|
||||
queryFn: () => fetchTodos(this.filter()),
|
||||
enabled: !!this.filter(),
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
[//]: # 'Example2'
|
||||
[//]: # 'Example3'
|
||||
|
||||
```angular-ts
|
||||
import { skipToken, injectQuery } from '@tanstack/angular-query-experimental'
|
||||
|
||||
@Component({
|
||||
selector: 'todos',
|
||||
template: `
|
||||
<div>
|
||||
// 🚀 applying the filter will enable and execute the query
|
||||
<filters-form onApply="filter.set" />
|
||||
<todos-table data="todosQuery.data()" />
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
export class TodosComponent {
|
||||
filter = signal('')
|
||||
|
||||
readonly todosQuery = injectQuery(() => ({
|
||||
queryKey: ['todos', this.filter()],
|
||||
queryFn: this.filter() ? () => fetchTodos(this.filter()) : skipToken,
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
[//]: # 'Example3'
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
id: does-this-replace-client-state
|
||||
title: Does TanStack Query replace global state managers?
|
||||
ref: docs/framework/react/guides/does-this-replace-client-state.md
|
||||
replace:
|
||||
{
|
||||
'useQuery': 'injectQuery',
|
||||
'useMutation': 'injectMutation',
|
||||
'hook': 'function',
|
||||
}
|
||||
---
|
||||
5
docs/framework/angular/guides/filters.md
Normal file
5
docs/framework/angular/guides/filters.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
id: filters
|
||||
title: Filters
|
||||
ref: docs/framework/react/guides/filters.md
|
||||
---
|
||||
16
docs/framework/angular/guides/important-defaults.md
Normal file
16
docs/framework/angular/guides/important-defaults.md
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
id: important-defaults
|
||||
title: Important Defaults
|
||||
ref: docs/framework/react/guides/important-defaults.md
|
||||
replace:
|
||||
{
|
||||
'React': 'Angular',
|
||||
'react-query': 'angular-query',
|
||||
'useQuery': 'injectQuery',
|
||||
'useInfiniteQuery': 'injectInfiniteQuery',
|
||||
'useMemo and useCallback': 'setting signal values',
|
||||
}
|
||||
---
|
||||
|
||||
[//]: # 'Materials'
|
||||
[//]: # 'Materials'
|
||||
161
docs/framework/angular/guides/infinite-queries.md
Normal file
161
docs/framework/angular/guides/infinite-queries.md
Normal file
@@ -0,0 +1,161 @@
|
||||
---
|
||||
id: infinite-queries
|
||||
title: Infinite Queries
|
||||
ref: docs/framework/react/guides/infinite-queries.md
|
||||
replace:
|
||||
{ 'useQuery': 'injectQuery', 'useInfiniteQuery': 'injectInfiniteQuery' }
|
||||
---
|
||||
|
||||
[//]: # 'Example'
|
||||
|
||||
```angular-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 {
|
||||
readonly projectsService = inject(ProjectsService)
|
||||
|
||||
readonly projectsQuery = 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.projectsQuery.hasNextPage
|
||||
#isFetchingNextPage = this.projectsQuery.isFetchingNextPage
|
||||
}
|
||||
```
|
||||
|
||||
```angular-html
|
||||
<div>
|
||||
@if (projectsQuery.isPending()) {
|
||||
<p>Loading...</p>
|
||||
} @else if (projectsQuery.isError()) {
|
||||
<span>Error: {{ projectsQuery?.error().message }}</span>
|
||||
} @else {
|
||||
@for (page of projectsQuery?.data().pages; track $index) {
|
||||
@for (project of page.data; track project.id) {
|
||||
<p>{{ project.name }} {{ project.id }}</p>
|
||||
}
|
||||
}
|
||||
<div>
|
||||
<button
|
||||
(click)="projectsQuery.fetchNextPage()"
|
||||
[disabled]="nextButtonDisabled()"
|
||||
>
|
||||
{{ nextButtonText() }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
```
|
||||
|
||||
[//]: # 'Example'
|
||||
[//]: # 'Example1'
|
||||
|
||||
```angular-ts
|
||||
@Component({
|
||||
template: ` <list-component (endReached)="fetchNextPage()" /> `,
|
||||
})
|
||||
export class Example {
|
||||
readonly projectsQuery = injectInfiniteQuery(() => ({
|
||||
queryKey: ['projects'],
|
||||
queryFn: async ({ pageParam }) => {
|
||||
return lastValueFrom(this.projectsService.getProjects(pageParam))
|
||||
},
|
||||
}))
|
||||
|
||||
fetchNextPage() {
|
||||
// Do nothing if already fetching
|
||||
if (this.projectsQuery.isFetching()) return
|
||||
this.projectsQuery.fetchNextPage()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
[//]: # 'Example1'
|
||||
[//]: # 'Example3'
|
||||
|
||||
```ts
|
||||
projectsQuery = injectInfiniteQuery(() => ({
|
||||
queryKey: ['projects'],
|
||||
queryFn: fetchProjects,
|
||||
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
|
||||
getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,
|
||||
}))
|
||||
```
|
||||
|
||||
[//]: # 'Example3'
|
||||
[//]: # 'Example4'
|
||||
|
||||
```ts
|
||||
projectsQuery = 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'
|
||||
132
docs/framework/angular/guides/initial-query-data.md
Normal file
132
docs/framework/angular/guides/initial-query-data.md
Normal file
@@ -0,0 +1,132 @@
|
||||
---
|
||||
id: initial-query-data
|
||||
title: Initial Query Data
|
||||
ref: docs/framework/react/guides/initial-query-data.md
|
||||
replace:
|
||||
{
|
||||
'render': 'service or component instance',
|
||||
' when it mounts': '',
|
||||
'after mount': 'after initialization',
|
||||
'on mount': 'on initialization',
|
||||
}
|
||||
---
|
||||
|
||||
[//]: # 'Example'
|
||||
|
||||
```ts
|
||||
todosQuery = injectQuery(() => ({
|
||||
queryKey: ['todos'],
|
||||
queryFn: () => fetch('/todos'),
|
||||
initialData: initialTodos,
|
||||
}))
|
||||
```
|
||||
|
||||
[//]: # 'Example'
|
||||
[//]: # 'Example2'
|
||||
|
||||
```ts
|
||||
// Will show initialTodos immediately, but also immediately refetch todos
|
||||
// when an instance of the component or service is created
|
||||
todosQuery = injectQuery(() => ({
|
||||
queryKey: ['todos'],
|
||||
queryFn: () => fetch('/todos'),
|
||||
initialData: initialTodos,
|
||||
}))
|
||||
```
|
||||
|
||||
[//]: # 'Example2'
|
||||
[//]: # 'Example3'
|
||||
|
||||
```ts
|
||||
// Show initialTodos immediately, but won't refetch until
|
||||
// another interaction event is encountered after 1000 ms
|
||||
todosQuery = injectQuery(() => ({
|
||||
queryKey: ['todos'],
|
||||
queryFn: () => fetch('/todos'),
|
||||
initialData: initialTodos,
|
||||
staleTime: 1000,
|
||||
}))
|
||||
```
|
||||
|
||||
[//]: # 'Example3'
|
||||
[//]: # 'Example4'
|
||||
|
||||
```ts
|
||||
// Show initialTodos immediately, but won't refetch until
|
||||
// another interaction event is encountered after 1000 ms
|
||||
todosQuery = injectQuery(() => ({
|
||||
queryKey: ['todos'],
|
||||
queryFn: () => fetch('/todos'),
|
||||
initialData: initialTodos,
|
||||
staleTime: 60 * 1000, // 1 minute
|
||||
// This could be 10 seconds ago or 10 minutes ago
|
||||
initialDataUpdatedAt: initialTodosUpdatedTimestamp, // eg. 1608412420052
|
||||
}))
|
||||
```
|
||||
|
||||
[//]: # 'Example4'
|
||||
[//]: # 'Example5'
|
||||
|
||||
```ts
|
||||
todosQuery = injectQuery(() => ({
|
||||
queryKey: ['todos'],
|
||||
queryFn: () => fetch('/todos'),
|
||||
initialData: () => getExpensiveTodos(),
|
||||
}))
|
||||
```
|
||||
|
||||
[//]: # 'Example5'
|
||||
[//]: # 'Example6'
|
||||
|
||||
```ts
|
||||
todoQuery = injectQuery(() => ({
|
||||
queryKey: ['todo', this.todoId()],
|
||||
queryFn: () => fetch('/todos'),
|
||||
initialData: () => {
|
||||
// Use a todo from the 'todos' query as the initial data for this todo query
|
||||
return this.queryClient
|
||||
.getQueryData(['todos'])
|
||||
?.find((d) => d.id === this.todoId())
|
||||
},
|
||||
}))
|
||||
```
|
||||
|
||||
[//]: # 'Example6'
|
||||
[//]: # 'Example7'
|
||||
|
||||
```ts
|
||||
todoQuery = injectQuery(() => ({
|
||||
queryKey: ['todos', this.todoId()],
|
||||
queryFn: () => fetch(`/todos/${this.todoId()}`),
|
||||
initialData: () =>
|
||||
queryClient.getQueryData(['todos'])?.find((d) => d.id === this.todoId()),
|
||||
initialDataUpdatedAt: () =>
|
||||
queryClient.getQueryState(['todos'])?.dataUpdatedAt,
|
||||
}))
|
||||
```
|
||||
|
||||
[//]: # 'Example7'
|
||||
[//]: # 'Example8'
|
||||
|
||||
```ts
|
||||
todoQuery = injectQuery(() => ({
|
||||
queryKey: ['todo', this.todoId()],
|
||||
queryFn: () => fetch(`/todos/${this.todoId()}`),
|
||||
initialData: () => {
|
||||
// Get the query state
|
||||
const state = queryClient.getQueryState(['todos'])
|
||||
|
||||
// If the query exists and has data that is no older than 10 seconds...
|
||||
if (state && Date.now() - state.dataUpdatedAt <= 10 * 1000) {
|
||||
// return the individual todo
|
||||
return state.data.find((d) => d.id === this.todoId())
|
||||
}
|
||||
|
||||
// Otherwise, return undefined and let it fetch from a hard loading state!
|
||||
},
|
||||
}))
|
||||
```
|
||||
|
||||
[//]: # 'Example8'
|
||||
[//]: # 'Materials'
|
||||
[//]: # 'Materials'
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
id: invalidations-from-mutations
|
||||
title: Invalidations from Mutations
|
||||
ref: docs/framework/react/guides/invalidations-from-mutations.md
|
||||
replace: { 'useMutation': 'injectMutation', 'hook': 'function' }
|
||||
---
|
||||
|
||||
[//]: # 'Example'
|
||||
|
||||
```ts
|
||||
postTodoMutation = injectMutation(() => ({
|
||||
mutationFn: postTodo,
|
||||
}))
|
||||
```
|
||||
|
||||
[//]: # 'Example'
|
||||
[//]: # 'Example2'
|
||||
|
||||
```ts
|
||||
import {
|
||||
injectMutation,
|
||||
QueryClient,
|
||||
} from '@tanstack/angular-query-experimental'
|
||||
|
||||
export class TodosComponent {
|
||||
readonly queryClient = inject(QueryClient)
|
||||
|
||||
// When this mutation succeeds, invalidate any queries with the `todos` or `reminders` query key
|
||||
readonly addTodoMutation = injectMutation(() => ({
|
||||
mutationFn: addTodo,
|
||||
onSuccess: () => {
|
||||
this.queryClient.invalidateQueries({ queryKey: ['todos'] })
|
||||
this.queryClient.invalidateQueries({ queryKey: ['reminders'] })
|
||||
},
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
[//]: # 'Example2'
|
||||
|
||||
You can wire up your invalidations to happen using any of the callbacks available in the [`injectMutation` function](./mutations.md)
|
||||
27
docs/framework/angular/guides/mutation-options.md
Normal file
27
docs/framework/angular/guides/mutation-options.md
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
id: query-options
|
||||
title: Mutation Options
|
||||
---
|
||||
|
||||
One of the best ways to share mutation options between multiple places,
|
||||
is to use the `mutationOptions` helper. At runtime, this helper just returns whatever you pass into it,
|
||||
but it has a lot of advantages when using it [with TypeScript](../typescript.md#typing-query-options).
|
||||
You can define all possible options for a mutation in one place,
|
||||
and you'll also get type inference and type safety for all of them.
|
||||
|
||||
```ts
|
||||
export class QueriesService {
|
||||
private readonly http = inject(HttpClient)
|
||||
|
||||
updatePost(id: number) {
|
||||
return mutationOptions({
|
||||
mutationFn: (post: Post) => Promise.resolve(post),
|
||||
mutationKey: ['updatePost', id],
|
||||
onSuccess: (newPost) => {
|
||||
// ^? newPost: Post
|
||||
this.queryClient.setQueryData(['posts', id], newPost)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
274
docs/framework/angular/guides/mutations.md
Normal file
274
docs/framework/angular/guides/mutations.md
Normal file
@@ -0,0 +1,274 @@
|
||||
---
|
||||
id: mutations
|
||||
title: Mutations
|
||||
ref: docs/framework/react/guides/mutations.md
|
||||
replace:
|
||||
{
|
||||
'useMutation': 'injectMutation',
|
||||
'hook': 'function',
|
||||
'still mounted': 'still active',
|
||||
'unmounts': 'gets destroyed',
|
||||
'mounted': 'initialized',
|
||||
}
|
||||
---
|
||||
|
||||
[//]: # 'Example'
|
||||
|
||||
```angular-ts
|
||||
@Component({
|
||||
template: `
|
||||
<div>
|
||||
@if (createTodoMutation.isPending()) {
|
||||
<span>Adding todo...</span>
|
||||
} @else if (createTodoMutation.isError()) {
|
||||
<div>An error occurred: {{ createTodoMutation.error()?.message }}</div>
|
||||
} @else if (createTodoMutation.isSuccess()) {
|
||||
<div>Todo added!</div>
|
||||
}
|
||||
<button (click)="createTodoMutation.mutate(1)">Create Todo</button>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
export class TodosComponent {
|
||||
readonly todoService = inject(TodoService)
|
||||
readonly createTodoMutation = injectMutation(() => ({
|
||||
mutationFn: (todoId: number) =>
|
||||
lastValueFrom(this.todoService.create(todoId)),
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
[//]: # 'Example'
|
||||
[//]: # 'Info1'
|
||||
[//]: # 'Info1'
|
||||
[//]: # 'Example2'
|
||||
[//]: # 'Example2'
|
||||
[//]: # 'Example3'
|
||||
|
||||
```angular-ts
|
||||
@Component({
|
||||
selector: 'todo-item',
|
||||
imports: [ReactiveFormsModule],
|
||||
template: `
|
||||
<form [formGroup]="todoForm" (ngSubmit)="onCreateTodo()">
|
||||
@if (createTodoMutation.error()) {
|
||||
<h5 (click)="createTodoMutation.reset()">
|
||||
{{ createTodoMutation.error() }}
|
||||
</h5>
|
||||
}
|
||||
<input type="text" formControlName="title" />
|
||||
<br />
|
||||
<button type="submit">Create Todo</button>
|
||||
</form>
|
||||
`,
|
||||
})
|
||||
export class TodosComponent {
|
||||
readonly createTodoMutation = injectMutation(() => ({
|
||||
mutationFn: createTodo,
|
||||
}))
|
||||
|
||||
readonly fb = inject(NonNullableFormBuilder)
|
||||
|
||||
todoForm = this.fb.group({
|
||||
title: this.fb.control('', {
|
||||
validators: [Validators.required],
|
||||
}),
|
||||
})
|
||||
|
||||
title = toSignal(this.todoForm.controls.title.valueChanges, {
|
||||
initialValue: '',
|
||||
})
|
||||
|
||||
onCreateTodo = () => {
|
||||
this.createTodoMutation.mutate(this.title())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
[//]: # 'Example3'
|
||||
[//]: # 'Example4'
|
||||
|
||||
```ts
|
||||
addTodoMutation = injectMutation(() => ({
|
||||
mutationFn: addTodo,
|
||||
onMutate: (variables, context) => {
|
||||
// A mutation is about to happen!
|
||||
|
||||
// Optionally return a result containing data to use when for example rolling back
|
||||
return { id: 1 }
|
||||
},
|
||||
onError: (error, variables, onMutateResult, context) => {
|
||||
// An error happened!
|
||||
console.log(`rolling back optimistic update with id ${onMutateResult.id}`)
|
||||
},
|
||||
onSuccess: (data, variables, onMutateResult, context) => {
|
||||
// Boom baby!
|
||||
},
|
||||
onSettled: (data, error, variables, onMutateResult, context) => {
|
||||
// Error or success... doesn't matter!
|
||||
},
|
||||
}))
|
||||
```
|
||||
|
||||
[//]: # 'Example4'
|
||||
[//]: # 'Example5'
|
||||
|
||||
```ts
|
||||
addTodoMutation = injectMutation(() => ({
|
||||
mutationFn: addTodo,
|
||||
onSuccess: async () => {
|
||||
console.log("I'm first!")
|
||||
},
|
||||
onSettled: async () => {
|
||||
console.log("I'm second!")
|
||||
},
|
||||
}))
|
||||
```
|
||||
|
||||
[//]: # 'Example5'
|
||||
[//]: # 'Example6'
|
||||
|
||||
```ts
|
||||
addTodoMutation = injectMutation(() => ({
|
||||
mutationFn: addTodo,
|
||||
onSuccess: (data, variables, onMutateResult, context) => {
|
||||
// I will fire first
|
||||
},
|
||||
onError: (error, variables, onMutateResult, context) => {
|
||||
// I will fire first
|
||||
},
|
||||
onSettled: (data, error, variables, onMutateResult, context) => {
|
||||
// I will fire first
|
||||
},
|
||||
}))
|
||||
|
||||
addTodoMutation.mutate(todo, {
|
||||
onSuccess: (data, variables, onMutateResult, context) => {
|
||||
// I will fire second!
|
||||
},
|
||||
onError: (error, variables, onMutateResult, context) => {
|
||||
// I will fire second!
|
||||
},
|
||||
onSettled: (data, error, variables, onMutateResult, context) => {
|
||||
// I will fire second!
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
[//]: # 'Example6'
|
||||
[//]: # 'Example7'
|
||||
|
||||
```ts
|
||||
export class Example {
|
||||
readonly addTodoMutation = injectMutation(() => ({
|
||||
mutationFn: addTodo,
|
||||
onSuccess: (data, variables, onMutateResult, context) => {
|
||||
// Will be called 3 times
|
||||
},
|
||||
}))
|
||||
|
||||
doMutations() {
|
||||
;['Todo 1', 'Todo 2', 'Todo 3'].forEach((todo) => {
|
||||
this.addTodoMutation.mutate(todo, {
|
||||
onSuccess: (data, variables, onMutateResult, context) => {
|
||||
// Will execute only once, for the last mutation (Todo 3),
|
||||
// regardless which mutation resolves first
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
[//]: # 'Example7'
|
||||
[//]: # 'Example8'
|
||||
|
||||
```ts
|
||||
addTodoMutation = injectMutation(() => ({ mutationFn: addTodo }))
|
||||
|
||||
try {
|
||||
const todo = await addTodoMutation.mutateAsync(todo)
|
||||
console.log(todo)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
console.log('done')
|
||||
}
|
||||
```
|
||||
|
||||
[//]: # 'Example8'
|
||||
[//]: # 'Example9'
|
||||
|
||||
```ts
|
||||
addTodoMutation = injectMutation(() => ({
|
||||
mutationFn: addTodo,
|
||||
retry: 3,
|
||||
}))
|
||||
```
|
||||
|
||||
[//]: # 'Example9'
|
||||
[//]: # 'Example10'
|
||||
|
||||
```ts
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
// Define the "addTodo" mutation
|
||||
queryClient.setMutationDefaults(['addTodo'], {
|
||||
mutationFn: addTodo,
|
||||
onMutate: async (variables, context) => {
|
||||
// Cancel current queries for the todos list
|
||||
await context.client.cancelQueries({ queryKey: ['todos'] })
|
||||
|
||||
// Create optimistic todo
|
||||
const optimisticTodo = { id: uuid(), title: variables.title }
|
||||
|
||||
// Add optimistic todo to todos list
|
||||
context.client.setQueryData(['todos'], (old) => [...old, optimisticTodo])
|
||||
|
||||
// Return result with the optimistic todo
|
||||
return { optimisticTodo }
|
||||
},
|
||||
onSuccess: (result, variables, onMutateResult, context) => {
|
||||
// Replace optimistic todo in the todos list with the result
|
||||
context.client.setQueryData(['todos'], (old) =>
|
||||
old.map((todo) =>
|
||||
todo.id === onMutateResult.optimisticTodo.id ? result : todo,
|
||||
),
|
||||
)
|
||||
},
|
||||
onError: (error, variables, onMutateResult, context) => {
|
||||
// Remove optimistic todo from the todos list
|
||||
context.client.setQueryData(['todos'], (old) =>
|
||||
old.filter((todo) => todo.id !== onMutateResult.optimisticTodo.id),
|
||||
)
|
||||
},
|
||||
retry: 3,
|
||||
})
|
||||
|
||||
class someComponent {
|
||||
// Start mutation in some component:
|
||||
readonly addTodoMutation = injectMutation(() => ({
|
||||
mutationKey: ['addTodo'],
|
||||
}))
|
||||
|
||||
someMethod() {
|
||||
addTodoMutation.mutate({ title: 'title' })
|
||||
}
|
||||
}
|
||||
|
||||
// If the mutation has been paused because the device is for example offline,
|
||||
// Then the paused mutation can be dehydrated when the application quits:
|
||||
const state = dehydrate(queryClient)
|
||||
|
||||
// The mutation can then be hydrated again when the application is started:
|
||||
hydrate(queryClient, state)
|
||||
|
||||
// Resume the paused mutations:
|
||||
queryClient.resumePausedMutations()
|
||||
```
|
||||
|
||||
[//]: # 'Example10'
|
||||
[//]: # 'Example11'
|
||||
[//]: # 'Example11'
|
||||
[//]: # 'Materials'
|
||||
[//]: # 'Materials'
|
||||
5
docs/framework/angular/guides/network-mode.md
Normal file
5
docs/framework/angular/guides/network-mode.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
id: network-mode
|
||||
title: Network Mode
|
||||
ref: docs/framework/react/guides/network-mode.md
|
||||
---
|
||||
168
docs/framework/angular/guides/optimistic-updates.md
Normal file
168
docs/framework/angular/guides/optimistic-updates.md
Normal file
@@ -0,0 +1,168 @@
|
||||
---
|
||||
id: optimistic-updates
|
||||
title: Optimistic Updates
|
||||
ref: docs/framework/react/guides/optimistic-updates.md
|
||||
replace:
|
||||
{
|
||||
'React': 'Angular',
|
||||
'useMutation': 'injectMutation',
|
||||
'hook': 'function',
|
||||
'useMutationState': 'injectMutationState',
|
||||
'addTodoMutation': 'addTodo',
|
||||
}
|
||||
---
|
||||
|
||||
[//]: # 'ExampleUI1'
|
||||
|
||||
```ts
|
||||
addTodo = injectMutation(() => ({
|
||||
mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }),
|
||||
// make sure to _return_ the Promise from the query invalidation
|
||||
// so that the mutation stays in `pending` state until the refetch is finished
|
||||
onSettled: async () => {
|
||||
return await queryClient.invalidateQueries({ queryKey: ['todos'] })
|
||||
},
|
||||
}))
|
||||
```
|
||||
|
||||
[//]: # 'ExampleUI1'
|
||||
[//]: # 'ExampleUI2'
|
||||
|
||||
```angular-ts
|
||||
@Component({
|
||||
template: `
|
||||
@for (todo of todos.data(); track todo.id) {
|
||||
<li>{{ todo.title }}</li>
|
||||
}
|
||||
@if (addTodo.isPending()) {
|
||||
<li style="opacity: 0.5">{{ addTodo.variables() }}</li>
|
||||
}
|
||||
`,
|
||||
})
|
||||
class TodosComponent {}
|
||||
```
|
||||
|
||||
[//]: # 'ExampleUI2'
|
||||
[//]: # 'ExampleUI3'
|
||||
|
||||
```angular-ts
|
||||
@Component({
|
||||
template: `
|
||||
@if (addTodo.isError()) {
|
||||
<li style="color: red">
|
||||
{{ addTodo.variables() }}
|
||||
<button (click)="addTodo.mutate(addTodo.variables())">Retry</button>
|
||||
</li>
|
||||
}
|
||||
`,
|
||||
})
|
||||
class TodosComponent {}
|
||||
```
|
||||
|
||||
[//]: # 'ExampleUI3'
|
||||
[//]: # 'ExampleUI4'
|
||||
|
||||
```ts
|
||||
// somewhere in your app
|
||||
addTodo = injectMutation(() => ({
|
||||
mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }),
|
||||
onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
|
||||
mutationKey: ['addTodo'],
|
||||
}))
|
||||
|
||||
// access variables somewhere else
|
||||
|
||||
mutationState = injectMutationState<string>(() => ({
|
||||
filters: { mutationKey: ['addTodo'], status: 'pending' },
|
||||
select: (mutation) => mutation.state.variables,
|
||||
}))
|
||||
```
|
||||
|
||||
[//]: # 'ExampleUI4'
|
||||
[//]: # 'Example'
|
||||
|
||||
```ts
|
||||
queryClient = inject(QueryClient)
|
||||
|
||||
updateTodo = injectMutation(() => ({
|
||||
mutationFn: updateTodo,
|
||||
// When mutate is called:
|
||||
onMutate: async (newTodo, context) => {
|
||||
// Cancel any outgoing refetches
|
||||
// (so they don't overwrite our optimistic update)
|
||||
await context.client.cancelQueries({ queryKey: ['todos'] })
|
||||
|
||||
// Snapshot the previous value
|
||||
const previousTodos = context.client.getQueryData(['todos'])
|
||||
|
||||
// Optimistically update to the new value
|
||||
context.client.setQueryData(['todos'], (old) => [...old, newTodo])
|
||||
|
||||
// Return a result object with the snapshotted value
|
||||
return { previousTodos }
|
||||
},
|
||||
// If the mutation fails,
|
||||
// use the result returned from onMutate to roll back
|
||||
onError: (err, newTodo, onMutateResult, context) => {
|
||||
context.client.setQueryData(['todos'], onMutateResult.previousTodos)
|
||||
},
|
||||
// Always refetch after error or success:
|
||||
onSettled: (data, error, variables, onMutateResult, context) => {
|
||||
context.client.invalidateQueries({ queryKey: ['todos'] })
|
||||
},
|
||||
}))
|
||||
```
|
||||
|
||||
[//]: # 'Example'
|
||||
[//]: # 'Example2'
|
||||
|
||||
```ts
|
||||
queryClient = inject(QueryClient)
|
||||
|
||||
updateTodo = injectMutation(() => ({
|
||||
mutationFn: updateTodo,
|
||||
// When mutate is called:
|
||||
onMutate: async (newTodo, context) => {
|
||||
// Cancel any outgoing refetches
|
||||
// (so they don't overwrite our optimistic update)
|
||||
await context.client.cancelQueries({ queryKey: ['todos', newTodo.id] })
|
||||
|
||||
// Snapshot the previous value
|
||||
const previousTodo = context.client.getQueryData(['todos', newTodo.id])
|
||||
|
||||
// Optimistically update to the new value
|
||||
context.client.setQueryData(['todos', newTodo.id], newTodo)
|
||||
|
||||
// Return a result with the previous and new todo
|
||||
return { previousTodo, newTodo }
|
||||
},
|
||||
// If the mutation fails, use the result we returned above
|
||||
onError: (err, newTodo, onMutateResult, context) => {
|
||||
context.client.setQueryData(
|
||||
['todos', onMutateResult.newTodo.id],
|
||||
onMutateResult.previousTodo,
|
||||
)
|
||||
},
|
||||
// Always refetch after error or success:
|
||||
onSettled: (newTodo, error, variables, onMutateResult, context) => {
|
||||
context.client.invalidateQueries({ queryKey: ['todos', newTodo.id] })
|
||||
},
|
||||
}))
|
||||
```
|
||||
|
||||
[//]: # 'Example2'
|
||||
[//]: # 'Example3'
|
||||
|
||||
```ts
|
||||
injectMutation({
|
||||
mutationFn: updateTodo,
|
||||
// ...
|
||||
onSettled: (newTodo, error, variables, onMutateResult, context) => {
|
||||
if (error) {
|
||||
// do something
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
[//]: # 'Example3'
|
||||
113
docs/framework/angular/guides/paginated-queries.md
Normal file
113
docs/framework/angular/guides/paginated-queries.md
Normal file
@@ -0,0 +1,113 @@
|
||||
---
|
||||
id: paginated-queries
|
||||
title: Paginated / Lagged Queries
|
||||
ref: docs/framework/react/guides/paginated-queries.md
|
||||
replace:
|
||||
{
|
||||
'useQuery': 'injectQuery',
|
||||
'useInfiniteQuery': 'injectInfiniteQuery',
|
||||
'hook': 'function',
|
||||
}
|
||||
---
|
||||
|
||||
[//]: # 'Example'
|
||||
|
||||
```ts
|
||||
const projectsQuery = injectQuery(() => ({
|
||||
queryKey: ['projects', page()],
|
||||
queryFn: fetchProjects,
|
||||
}))
|
||||
```
|
||||
|
||||
[//]: # 'Example'
|
||||
[//]: # 'Example2'
|
||||
|
||||
```angular-ts
|
||||
@Component({
|
||||
selector: 'pagination-example',
|
||||
template: `
|
||||
<div>
|
||||
<p>
|
||||
In this example, each page of data remains visible as the next page is
|
||||
fetched. The buttons and capability to proceed to the next page are also
|
||||
suppressed until the next page cursor is known. Each page is cached as a
|
||||
normal query too, so when going to previous pages, you'll see them
|
||||
instantaneously while they are also re-fetched invisibly in the
|
||||
background.
|
||||
</p>
|
||||
@if (projectsQuery.status() === 'pending') {
|
||||
<div>Loading...</div>
|
||||
} @else if (projectsQuery.status() === 'error') {
|
||||
<div>Error: {{ projectsQuery.error().message }}</div>
|
||||
} @else {
|
||||
<!-- 'data' will either resolve to the latest page's data -->
|
||||
<!-- or if fetching a new page, the last successful page's data -->
|
||||
<div>
|
||||
@for (project of projectsQuery.data().projects; track project.id) {
|
||||
<p>{{ project.name }}</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div>Current Page: {{ page() + 1 }}</div>
|
||||
<button (click)="previousPage()" [disabled]="page() === 0">
|
||||
Previous Page
|
||||
</button>
|
||||
<button
|
||||
(click)="nextPage()"
|
||||
[disabled]="
|
||||
projectsQuery.isPlaceholderData() || !projectsQuery.data()?.hasMore
|
||||
"
|
||||
>
|
||||
Next Page
|
||||
</button>
|
||||
<!-- Since the last page's data potentially sticks around between page requests, -->
|
||||
<!-- we can use 'isFetching' to show a background loading -->
|
||||
<!-- indicator since our status === 'pending' state won't be triggered -->
|
||||
@if (projectsQuery.isFetching()) {
|
||||
<span> Loading...</span>
|
||||
}
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
export class PaginationExampleComponent {
|
||||
page = signal(0)
|
||||
readonly #queryClient = inject(QueryClient)
|
||||
|
||||
readonly projectsQuery = injectQuery(() => ({
|
||||
queryKey: ['projects', this.page()],
|
||||
queryFn: () => lastValueFrom(fetchProjects(this.page())),
|
||||
placeholderData: keepPreviousData,
|
||||
staleTime: 5000,
|
||||
}))
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
// Prefetch the next page!
|
||||
if (
|
||||
!this.projectsQuery.isPlaceholderData() &&
|
||||
this.projectsQuery.data()?.hasMore
|
||||
) {
|
||||
void this.#queryClient
|
||||
.query({
|
||||
queryKey: ['projects', this.page() + 1],
|
||||
queryFn: () => lastValueFrom(fetchProjects(this.page() + 1)),
|
||||
})
|
||||
.catch(noop)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
previousPage() {
|
||||
this.page.update((old) => Math.max(old - 1, 0))
|
||||
}
|
||||
|
||||
nextPage() {
|
||||
this.page.update((old) =>
|
||||
this.projectsQuery.data()?.hasMore ? old + 1 : old,
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
[//]: # 'Example2'
|
||||
63
docs/framework/angular/guides/parallel-queries.md
Normal file
63
docs/framework/angular/guides/parallel-queries.md
Normal file
@@ -0,0 +1,63 @@
|
||||
---
|
||||
id: parallel-queries
|
||||
title: Parallel Queries
|
||||
ref: docs/framework/react/guides/parallel-queries.md
|
||||
replace:
|
||||
{
|
||||
'If the number of queries you need to execute is changing from render to render, you cannot use manual querying since that would violate the rules of hooks. Instead, ': '',
|
||||
'hook': 'function',
|
||||
'React': 'Angular',
|
||||
'hooks': 'functions',
|
||||
'useQuery': 'injectQuery',
|
||||
'useInfiniteQuery': 'injectInfiniteQuery',
|
||||
'useQueries': 'injectQueries',
|
||||
}
|
||||
---
|
||||
|
||||
[//]: # 'Example'
|
||||
|
||||
```ts
|
||||
export class AppComponent {
|
||||
// The following queries will execute in parallel
|
||||
readonly usersQuery = injectQuery(() => ({
|
||||
queryKey: ['users'],
|
||||
queryFn: fetchUsers,
|
||||
}))
|
||||
readonly teamsQuery = injectQuery(() => ({
|
||||
queryKey: ['teams'],
|
||||
queryFn: fetchTeams,
|
||||
}))
|
||||
readonly projectsQuery = injectQuery(() => ({
|
||||
queryKey: ['projects'],
|
||||
queryFn: fetchProjects,
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
[//]: # 'Example'
|
||||
[//]: # 'Info'
|
||||
[//]: # 'Info'
|
||||
[//]: # 'DynamicParallelIntro'
|
||||
|
||||
TanStack Query provides `injectQueries`, which you can use to dynamically execute as many queries in parallel as you'd like.
|
||||
|
||||
[//]: # 'DynamicParallelIntro'
|
||||
[//]: # 'Example2'
|
||||
|
||||
```ts
|
||||
export class AppComponent {
|
||||
users = signal<Array<User>>([])
|
||||
|
||||
// Please note injectQueries is under development and this code does not work yet
|
||||
readonly userQueries = injectQueries(() => ({
|
||||
queries: users().map((user) => {
|
||||
return {
|
||||
queryKey: ['user', user.id],
|
||||
queryFn: () => fetchUserById(user.id),
|
||||
}
|
||||
}),
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
[//]: # 'Example2'
|
||||
58
docs/framework/angular/guides/placeholder-query-data.md
Normal file
58
docs/framework/angular/guides/placeholder-query-data.md
Normal file
@@ -0,0 +1,58 @@
|
||||
---
|
||||
id: placeholder-query-data
|
||||
title: Placeholder Query Data
|
||||
ref: docs/framework/react/guides/placeholder-query-data.md
|
||||
---
|
||||
|
||||
[//]: # 'ExampleValue'
|
||||
|
||||
```ts
|
||||
class TodosComponent {
|
||||
readonly todosQuery = injectQuery(() => ({
|
||||
queryKey: ['todos'],
|
||||
queryFn: () => fetch('/todos'),
|
||||
placeholderData: placeholderTodos,
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
[//]: # 'ExampleValue'
|
||||
[//]: # 'Memoization'
|
||||
[//]: # 'Memoization'
|
||||
[//]: # 'ExampleFunction'
|
||||
|
||||
```ts
|
||||
class TodosComponent {
|
||||
readonly todoQuery = injectQuery(() => ({
|
||||
queryKey: ['todos', id()],
|
||||
queryFn: () => fetch(`/todos/${id}`),
|
||||
placeholderData: (previousData, previousQuery) => previousData,
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
[//]: # 'ExampleFunction'
|
||||
[//]: # 'ExampleCache'
|
||||
|
||||
```ts
|
||||
export class BlogPostComponent {
|
||||
postId = input.required<number>()
|
||||
readonly queryClient = inject(QueryClient)
|
||||
|
||||
readonly blogPostQuery = injectQuery(() => ({
|
||||
queryKey: ['blogPost', this.postId()],
|
||||
queryFn: () => fetch(`/blogPosts/${this.postId()}`),
|
||||
placeholderData: () => {
|
||||
// Use the smaller/preview version of the blogPost from the 'blogPosts'
|
||||
// query as the placeholder data for this blogPost query
|
||||
return this.queryClient
|
||||
.getQueryData(['blogPosts'])
|
||||
?.find((d) => d.id === this.postId())
|
||||
},
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
[//]: # 'ExampleCache'
|
||||
[//]: # 'Materials'
|
||||
[//]: # 'Materials'
|
||||
104
docs/framework/angular/guides/queries.md
Normal file
104
docs/framework/angular/guides/queries.md
Normal file
@@ -0,0 +1,104 @@
|
||||
---
|
||||
id: queries
|
||||
title: Queries
|
||||
ref: docs/framework/react/guides/queries.md
|
||||
replace:
|
||||
{
|
||||
'React': 'Angular',
|
||||
'react-query': 'angular-query',
|
||||
'promise': 'promise or observable',
|
||||
'custom hooks': 'services',
|
||||
'the `useQuery` hook': '`injectQuery`',
|
||||
'`useQuery`': '`injectQuery`',
|
||||
"TypeScript will also narrow the type of data correctly if you've checked for pending and error before accessing it.": 'TypeScript will only narrow the type when checking boolean signals such as `isPending` and `isError`.',
|
||||
}
|
||||
---
|
||||
|
||||
[//]: # 'Example'
|
||||
|
||||
```ts
|
||||
import { injectQuery } from '@tanstack/angular-query-experimental'
|
||||
|
||||
export class TodosComponent {
|
||||
readonly todosQuery = injectQuery(() => ({
|
||||
queryKey: ['todos'],
|
||||
queryFn: fetchTodoList,
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
[//]: # 'Example'
|
||||
[//]: # 'Example2'
|
||||
|
||||
```ts
|
||||
todosQuery = injectQuery(() => ({
|
||||
queryKey: ['todos'],
|
||||
queryFn: fetchTodoList,
|
||||
}))
|
||||
```
|
||||
|
||||
[//]: # 'Example2'
|
||||
[//]: # 'Example3'
|
||||
|
||||
```angular-ts
|
||||
@Component({
|
||||
selector: 'todos',
|
||||
template: `
|
||||
@if (todosQuery.isPending()) {
|
||||
<span>Loading...</span>
|
||||
} @else if (todosQuery.isError()) {
|
||||
<span>Error: {{ todosQuery.error()?.message }}</span>
|
||||
} @else {
|
||||
<!-- We can assume by this point that status === 'success' -->
|
||||
@for (todo of todosQuery.data(); track todo.id) {
|
||||
<li>{{ todo.title }}</li>
|
||||
} @empty {
|
||||
<li>No todos found</li>
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
export class PostsComponent {
|
||||
readonly todosQuery = injectQuery(() => ({
|
||||
queryKey: ['todos'],
|
||||
queryFn: fetchTodoList,
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
[//]: # 'Example3'
|
||||
|
||||
If booleans aren't your thing, you can always use the `status` state as well:
|
||||
|
||||
[//]: # 'Example4'
|
||||
|
||||
```angular-ts
|
||||
@Component({
|
||||
selector: 'todos',
|
||||
template: `
|
||||
@switch (todosQuery.status()) {
|
||||
@case ('pending') {
|
||||
<span>Loading...</span>
|
||||
}
|
||||
@case ('error') {
|
||||
<span>Error: {{ todosQuery.error()?.message }}</span>
|
||||
}
|
||||
<!-- also status === 'success', but "else" logic works, too -->
|
||||
@default {
|
||||
<ul>
|
||||
@for (todo of todosQuery.data(); track todo.id) {
|
||||
<li>{{ todo.title }}</li>
|
||||
} @empty {
|
||||
<li>No todos found</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
class TodosComponent {}
|
||||
```
|
||||
|
||||
[//]: # 'Example4'
|
||||
[//]: # 'Materials'
|
||||
[//]: # 'Materials'
|
||||
105
docs/framework/angular/guides/query-cancellation.md
Normal file
105
docs/framework/angular/guides/query-cancellation.md
Normal file
@@ -0,0 +1,105 @@
|
||||
---
|
||||
id: query-cancellation
|
||||
title: Query Cancellation
|
||||
---
|
||||
|
||||
TanStack Query provides each query function with an [`AbortSignal` instance](https://developer.mozilla.org/docs/Web/API/AbortSignal). When a query becomes out-of-date or inactive, this `signal` will become aborted. This means that all queries are cancellable, and you can respond to the cancellation inside your query function if desired. The best part about this is that it allows you to continue to use normal async/await syntax while getting all the benefits of automatic cancellation.
|
||||
|
||||
## Default behavior
|
||||
|
||||
By default, queries that unmount or become unused before their promises are resolved are _not_ cancelled. This means that after the promise has resolved, the resulting data will be available in the cache. This is helpful if you've started receiving a query, but then unmount the component before it finishes. If you mount the component again and the query has not been garbage collected yet, data will be available.
|
||||
|
||||
However, if you consume the `AbortSignal`, the Promise will be cancelled (e.g. aborting the fetch) and therefore, also the Query must be cancelled. Cancelling the query will result in its state being _reverted_ to its previous state.
|
||||
|
||||
## Using `HttpClient`
|
||||
|
||||
```ts
|
||||
import { HttpClient } from '@angular/common/http'
|
||||
import { injectQuery } from '@tanstack/angular-query-experimental'
|
||||
|
||||
postQuery = injectQuery(() => ({
|
||||
enabled: this.postId() > 0,
|
||||
queryKey: ['post', this.postId()],
|
||||
queryFn: async (context): Promise<Post> => {
|
||||
const abort$ = fromEvent(context.signal, 'abort')
|
||||
return lastValueFrom(this.getPost$(this.postId()).pipe(takeUntil(abort$)))
|
||||
},
|
||||
}))
|
||||
```
|
||||
|
||||
## Using `fetch`
|
||||
|
||||
[//]: # 'Example2'
|
||||
|
||||
```ts
|
||||
todosQuery = injectQuery(() => ({
|
||||
queryKey: ['todos'],
|
||||
queryFn: async ({ signal }) => {
|
||||
const todosResponse = await fetch('/todos', {
|
||||
// Pass the signal to one fetch
|
||||
signal,
|
||||
})
|
||||
const todos = await todosResponse.json()
|
||||
|
||||
const todoDetails = todos.map(async ({ details }) => {
|
||||
const response = await fetch(details, {
|
||||
// Or pass it to several
|
||||
signal,
|
||||
})
|
||||
return response.json()
|
||||
})
|
||||
|
||||
return Promise.all(todoDetails)
|
||||
},
|
||||
}))
|
||||
```
|
||||
|
||||
[//]: # 'Example2'
|
||||
|
||||
## Using `axios`
|
||||
|
||||
[//]: # 'Example3'
|
||||
|
||||
```ts
|
||||
import axios from 'axios'
|
||||
|
||||
const todosQuery = injectQuery(() => ({
|
||||
queryKey: ['todos'],
|
||||
queryFn: ({ signal }) =>
|
||||
axios.get('/todos', {
|
||||
// Pass the signal to `axios`
|
||||
signal,
|
||||
}),
|
||||
}))
|
||||
```
|
||||
|
||||
[//]: # 'Example3'
|
||||
|
||||
## Manual Cancellation
|
||||
|
||||
You might want to cancel a query manually. For example, if the request takes a long time to finish, you can allow the user to click a cancel button to stop the request. To do this, you just need to call `queryClient.cancelQueries({ queryKey })`, which will cancel the query and revert it back to its previous state. If you have consumed the `signal` passed to the query function, TanStack Query will additionally also cancel the Promise.
|
||||
|
||||
[//]: # 'Example7'
|
||||
|
||||
```angular-ts
|
||||
@Component({
|
||||
template: `<button (click)="onCancel()">Cancel</button>`,
|
||||
})
|
||||
export class TodosComponent {
|
||||
readonly todosQuery = injectQuery(() => ({
|
||||
queryKey: ['todos'],
|
||||
queryFn: async ({ signal }) => {
|
||||
const resp = await fetch('/todos', { signal })
|
||||
return resp.json()
|
||||
},
|
||||
}))
|
||||
|
||||
readonly queryClient = inject(QueryClient)
|
||||
|
||||
onCancel() {
|
||||
this.queryClient.cancelQueries(['todos'])
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
[//]: # 'Example7'
|
||||
79
docs/framework/angular/guides/query-functions.md
Normal file
79
docs/framework/angular/guides/query-functions.md
Normal file
@@ -0,0 +1,79 @@
|
||||
---
|
||||
id: query-functions
|
||||
title: Query Functions
|
||||
ref: docs/framework/react/guides/query-functions.md
|
||||
---
|
||||
|
||||
[//]: # 'Example'
|
||||
|
||||
```ts
|
||||
injectQuery(() => ({ queryKey: ['todos'], queryFn: fetchAllTodos }))
|
||||
injectQuery(() => ({
|
||||
queryKey: ['todos', todoId],
|
||||
queryFn: () => fetchTodoById(todoId),
|
||||
}))
|
||||
injectQuery(() => ({
|
||||
queryKey: ['todos', todoId],
|
||||
queryFn: async () => {
|
||||
const data = await fetchTodoById(todoId)
|
||||
return data
|
||||
},
|
||||
}))
|
||||
injectQuery(() => ({
|
||||
queryKey: ['todos', todoId],
|
||||
queryFn: ({ queryKey }) => fetchTodoById(queryKey[1]),
|
||||
}))
|
||||
```
|
||||
|
||||
[//]: # 'Example'
|
||||
[//]: # 'Example2'
|
||||
|
||||
```ts
|
||||
todoQuery = injectQuery(() => ({
|
||||
queryKey: ['todos', todoId()],
|
||||
queryFn: async () => {
|
||||
if (somethingGoesWrong) {
|
||||
throw new Error('Oh no!')
|
||||
}
|
||||
if (somethingElseGoesWrong) {
|
||||
return Promise.reject(new Error('Oh no!'))
|
||||
}
|
||||
|
||||
return data
|
||||
},
|
||||
}))
|
||||
```
|
||||
|
||||
[//]: # 'Example2'
|
||||
[//]: # 'Example3'
|
||||
|
||||
```ts
|
||||
todoQuery = injectQuery(() => ({
|
||||
queryKey: ['todos', todoId()],
|
||||
queryFn: async () => {
|
||||
const response = await fetch('/todos/' + todoId)
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok')
|
||||
}
|
||||
return response.json()
|
||||
},
|
||||
}))
|
||||
```
|
||||
|
||||
[//]: # 'Example3'
|
||||
[//]: # 'Example4'
|
||||
|
||||
```ts
|
||||
todosQuery = injectQuery(() => ({
|
||||
queryKey: ['todos', { status: status(), page: page() }],
|
||||
queryFn: fetchTodoList,
|
||||
}))
|
||||
|
||||
// Access the key, status and page variables in your query function!
|
||||
function fetchTodoList({ queryKey }) {
|
||||
const [_key, { status, page }] = queryKey
|
||||
return new Promise()
|
||||
}
|
||||
```
|
||||
|
||||
[//]: # 'Example4'
|
||||
112
docs/framework/angular/guides/query-invalidation.md
Normal file
112
docs/framework/angular/guides/query-invalidation.md
Normal file
@@ -0,0 +1,112 @@
|
||||
---
|
||||
id: query-invalidation
|
||||
title: Query Invalidation
|
||||
ref: docs/framework/react/guides/query-invalidation.md
|
||||
replace: { 'useQuery': 'injectQuery', 'hooks': 'functions' }
|
||||
---
|
||||
|
||||
[//]: # 'Example2'
|
||||
|
||||
```ts
|
||||
import { injectQuery, QueryClient } from '@tanstack/angular-query-experimental'
|
||||
|
||||
class QueryInvalidationExample {
|
||||
readonly queryClient = inject(QueryClient)
|
||||
|
||||
invalidateQueries() {
|
||||
this.queryClient.invalidateQueries({ queryKey: ['todos'] })
|
||||
}
|
||||
|
||||
// Both queries below will be invalidated
|
||||
todoListQuery = injectQuery(() => ({
|
||||
queryKey: ['todos'],
|
||||
queryFn: fetchTodoList,
|
||||
}))
|
||||
todoListQuery = injectQuery(() => ({
|
||||
queryKey: ['todos', { page: 1 }],
|
||||
queryFn: fetchTodoList,
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
[//]: # 'Example2'
|
||||
|
||||
You can even invalidate queries with specific variables by passing a more specific query key to the `invalidateQueries` method:
|
||||
|
||||
[//]: # 'Example3'
|
||||
|
||||
```ts
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['todos', { type: 'done' }],
|
||||
})
|
||||
|
||||
// The query below will be invalidated
|
||||
todoListQuery = injectQuery(() => ({
|
||||
queryKey: ['todos', { type: 'done' }],
|
||||
queryFn: fetchTodoList,
|
||||
}))
|
||||
|
||||
// However, the following query below will NOT be invalidated
|
||||
todoListQuery = injectQuery(() => ({
|
||||
queryKey: ['todos'],
|
||||
queryFn: fetchTodoList,
|
||||
}))
|
||||
```
|
||||
|
||||
[//]: # 'Example3'
|
||||
|
||||
The `invalidateQueries` API is very flexible, so even if you want to **only** invalidate `todos` queries that don't have any more variables or subkeys, you can pass an `exact: true` option to the `invalidateQueries` method:
|
||||
|
||||
[//]: # 'Example4'
|
||||
|
||||
```ts
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['todos'],
|
||||
exact: true,
|
||||
})
|
||||
|
||||
// The query below will be invalidated
|
||||
todoListQuery = injectQuery(() => ({
|
||||
queryKey: ['todos'],
|
||||
queryFn: fetchTodoList,
|
||||
}))
|
||||
|
||||
// However, the following query below will NOT be invalidated
|
||||
const todoListQuery = injectQuery(() => ({
|
||||
queryKey: ['todos', { type: 'done' }],
|
||||
queryFn: fetchTodoList,
|
||||
}))
|
||||
```
|
||||
|
||||
[//]: # 'Example4'
|
||||
|
||||
If you find yourself wanting **even more** granularity, you can pass a predicate function to the `invalidateQueries` method. This function will receive each `Query` instance from the query cache and allow you to return `true` or `false` for whether you want to invalidate that query:
|
||||
|
||||
[//]: # 'Example5'
|
||||
|
||||
```ts
|
||||
queryClient.invalidateQueries({
|
||||
predicate: (query) =>
|
||||
query.queryKey[0] === 'todos' && query.queryKey[1]?.version >= 10,
|
||||
})
|
||||
|
||||
// The query below will be invalidated
|
||||
todoListQuery = injectQuery(() => ({
|
||||
queryKey: ['todos', { version: 20 }],
|
||||
queryFn: fetchTodoList,
|
||||
}))
|
||||
|
||||
// The query below will be invalidated
|
||||
todoListQuery = injectQuery(() => ({
|
||||
queryKey: ['todos', { version: 10 }],
|
||||
queryFn: fetchTodoList,
|
||||
}))
|
||||
|
||||
// However, the following query below will NOT be invalidated
|
||||
todoListQuery = injectQuery(() => ({
|
||||
queryKey: ['todos', { version: 5 }],
|
||||
queryFn: fetchTodoList,
|
||||
}))
|
||||
```
|
||||
|
||||
[//]: # 'Example5'
|
||||
65
docs/framework/angular/guides/query-keys.md
Normal file
65
docs/framework/angular/guides/query-keys.md
Normal file
@@ -0,0 +1,65 @@
|
||||
---
|
||||
id: query-keys
|
||||
title: Query Keys
|
||||
ref: docs/framework/react/guides/query-keys.md
|
||||
#todo: exhaustive-deps is at least for now React-only
|
||||
---
|
||||
|
||||
[//]: # 'Example'
|
||||
|
||||
```ts
|
||||
// A list of todos
|
||||
injectQuery(() => ({ queryKey: ['todos'], ... }))
|
||||
|
||||
// Something else, whatever!
|
||||
injectQuery(() => ({ queryKey: ['something', 'special'], ... }))
|
||||
```
|
||||
|
||||
[//]: # 'Example'
|
||||
[//]: # 'Example2'
|
||||
|
||||
```ts
|
||||
// An individual todo
|
||||
injectQuery(() => ({queryKey: ['todo', 5], ...}))
|
||||
|
||||
// An individual todo in a "preview" format
|
||||
injectQuery(() => ({queryKey: ['todo', 5, {preview: true}], ...}))
|
||||
|
||||
// A list of todos that are "done"
|
||||
injectQuery(() => ({queryKey: ['todos', {type: 'done'}], ...}))
|
||||
```
|
||||
|
||||
[//]: # 'Example2'
|
||||
[//]: # 'Example3'
|
||||
|
||||
```ts
|
||||
injectQuery(() => ({ queryKey: ['todos', { status, page }], ... }))
|
||||
injectQuery(() => ({ queryKey: ['todos', { page, status }], ...}))
|
||||
injectQuery(() => ({ queryKey: ['todos', { page, status, other: undefined }], ... }))
|
||||
```
|
||||
|
||||
[//]: # 'Example3'
|
||||
[//]: # 'Example4'
|
||||
|
||||
```ts
|
||||
injectQuery(() => ({ queryKey: ['todos', status, page], ... }))
|
||||
injectQuery(() => ({ queryKey: ['todos', page, status], ...}))
|
||||
injectQuery(() => ({ queryKey: ['todos', undefined, page, status], ...}))
|
||||
```
|
||||
|
||||
[//]: # 'Example4'
|
||||
[//]: # 'Example5'
|
||||
|
||||
```ts
|
||||
todoId = signal(-1)
|
||||
|
||||
injectQuery(() => ({
|
||||
enabled: todoId() > 0,
|
||||
queryKey: ['todos', todoId()],
|
||||
queryFn: () => fetchTodoById(todoId()),
|
||||
}))
|
||||
```
|
||||
|
||||
[//]: # 'Example5'
|
||||
[//]: # 'Materials'
|
||||
[//]: # 'Materials'
|
||||
58
docs/framework/angular/guides/query-options.md
Normal file
58
docs/framework/angular/guides/query-options.md
Normal file
@@ -0,0 +1,58 @@
|
||||
---
|
||||
id: query-options
|
||||
title: Query Options
|
||||
ref: docs/framework/react/guides/query-options.md
|
||||
---
|
||||
|
||||
[//]: # 'Example1'
|
||||
|
||||
```ts
|
||||
import { queryOptions, noop } from '@tanstack/angular-query-experimental'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class QueriesService {
|
||||
private readonly http = inject(HttpClient)
|
||||
|
||||
post(postId: number) {
|
||||
return queryOptions({
|
||||
queryKey: ['post', postId],
|
||||
queryFn: () => {
|
||||
return lastValueFrom(
|
||||
this.http.get<Post>(
|
||||
`https://jsonplaceholder.typicode.com/posts/${postId}`,
|
||||
),
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// usage:
|
||||
|
||||
postId = input.required({
|
||||
transform: numberAttribute,
|
||||
})
|
||||
queries = inject(QueriesService)
|
||||
|
||||
postQuery = injectQuery(() => this.queries.post(this.postId()))
|
||||
|
||||
queryClient.query(this.queries.post(23)).catch(noop)
|
||||
queryClient.setQueryData(this.queries.post(42).queryKey, newPost)
|
||||
```
|
||||
|
||||
[//]: # 'Example1'
|
||||
[//]: # 'Example2'
|
||||
|
||||
```ts
|
||||
// Type inference still works, so groupQuery.data will be the return type of select instead of queryFn
|
||||
queries = inject(QueriesService)
|
||||
|
||||
groupQuery = injectQuery(() => ({
|
||||
...groupOptions(1),
|
||||
select: (data) => data.title,
|
||||
}))
|
||||
```
|
||||
|
||||
[//]: # 'Example2'
|
||||
66
docs/framework/angular/guides/query-retries.md
Normal file
66
docs/framework/angular/guides/query-retries.md
Normal file
@@ -0,0 +1,66 @@
|
||||
---
|
||||
id: query-retries
|
||||
title: Query Retries
|
||||
ref: docs/framework/react/guides/query-retries.md
|
||||
replace:
|
||||
{
|
||||
'Provider': 'Plugin',
|
||||
'useQuery': 'injectQuery',
|
||||
'useMutation': 'injectMutation',
|
||||
}
|
||||
---
|
||||
|
||||
[//]: # 'Info'
|
||||
[//]: # 'Info'
|
||||
[//]: # 'Example'
|
||||
|
||||
```ts
|
||||
import { injectQuery } from '@tanstack/angular-query-experimental'
|
||||
|
||||
// Make a specific query retry a certain number of times
|
||||
const todoQuery = injectQuery(() => ({
|
||||
queryKey: ['todos', 1],
|
||||
queryFn: fetchTodoListPage,
|
||||
retry: 10, // Will retry failed requests 10 times before displaying an error
|
||||
}))
|
||||
```
|
||||
|
||||
[//]: # 'Example'
|
||||
[//]: # 'Example2'
|
||||
|
||||
```ts
|
||||
// Configure for all queries
|
||||
import {
|
||||
QueryCache,
|
||||
QueryClient,
|
||||
provideTanStackQuery,
|
||||
} from '@tanstack/angular-query-experimental'
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
bootstrapApplication(AppComponent, {
|
||||
providers: [provideTanStackQuery(queryClient)],
|
||||
})
|
||||
```
|
||||
|
||||
[//]: # 'Example2'
|
||||
|
||||
Though it is not recommended, you can obviously override the `retryDelay` function/integer in both the Provider and individual query options. If set to an integer instead of a function the delay will always be the same amount of time:
|
||||
|
||||
[//]: # 'Example3'
|
||||
|
||||
```ts
|
||||
const todosQuery = injectQuery(() => ({
|
||||
queryKey: ['todos'],
|
||||
queryFn: fetchTodoList,
|
||||
retryDelay: 1000, // Will always wait 1000ms to retry, regardless of how many retries
|
||||
}))
|
||||
```
|
||||
|
||||
[//]: # 'Example3'
|
||||
5
docs/framework/angular/guides/scroll-restoration.md
Normal file
5
docs/framework/angular/guides/scroll-restoration.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
id: scroll-restoration
|
||||
title: Scroll Restoration
|
||||
ref: docs/framework/react/guides/scroll-restoration.md
|
||||
---
|
||||
157
docs/framework/angular/guides/testing.md
Normal file
157
docs/framework/angular/guides/testing.md
Normal file
@@ -0,0 +1,157 @@
|
||||
---
|
||||
id: testing
|
||||
title: Testing
|
||||
---
|
||||
|
||||
Most Angular tests using TanStack Query will involve services or components that call `injectQuery`/`injectMutation`.
|
||||
|
||||
TanStack Query's `inject*` functions integrate with [`PendingTasks`](https://angular.dev/api/core/PendingTasks) which ensures the framework is aware of in-progress queries and mutations.
|
||||
|
||||
This means tests and SSR can wait until mutations and queries resolve. In unit tests you can use `ApplicationRef.whenStable()` or `fixture.whenStable()` to await query completion. This works for both Zone.js and Zoneless setups.
|
||||
|
||||
> This integration requires Angular 19 or later. Earlier versions of Angular do not support `PendingTasks`.
|
||||
|
||||
## TestBed setup
|
||||
|
||||
Create a fresh `QueryClient` for every spec and provide it with `provideTanStackQuery` or `provideQueryClient`. This keeps caches isolated and lets you change default options per test:
|
||||
|
||||
```ts
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false, // ✅ faster failure tests
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideTanStackQuery(queryClient)],
|
||||
})
|
||||
```
|
||||
|
||||
> If your applications actual TanStack Query config is used in unit tests, make sure `withDevtools` is not accidentally included in test providers. This can cause slow tests. It is best to keep test and production configs separate.
|
||||
|
||||
If you share helpers, remember to call `queryClient.clear()` (or build a new instance) in `afterEach` so data from one test never bleeds into another.
|
||||
|
||||
## First query test
|
||||
|
||||
Query tests typically run inside `TestBed.runInInjectionContext`, then wait for stability:
|
||||
|
||||
```ts
|
||||
const appRef = TestBed.inject(ApplicationRef)
|
||||
const query = TestBed.runInInjectionContext(() =>
|
||||
injectQuery(() => ({
|
||||
queryKey: ['greeting'],
|
||||
queryFn: () => 'Hello',
|
||||
})),
|
||||
)
|
||||
|
||||
TestBed.tick() // Trigger effect
|
||||
|
||||
// Application is stable when queries are idle
|
||||
await appRef.whenStable()
|
||||
|
||||
expect(query.status()).toBe('success')
|
||||
expect(query.data()).toBe('Hello')
|
||||
```
|
||||
|
||||
PendingTasks will have `whenStable()` resolve after the query settles. When using fake timers (Vitest), advance the clock and a microtask before awaiting stability:
|
||||
|
||||
```ts
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
await Promise.resolve()
|
||||
await appRef.whenStable()
|
||||
```
|
||||
|
||||
## Testing components
|
||||
|
||||
For components, bootstrap them through `TestBed.createComponent`, then await `fixture.whenStable()`:
|
||||
|
||||
```ts
|
||||
const fixture = TestBed.createComponent(ExampleComponent)
|
||||
|
||||
await fixture.whenStable()
|
||||
expect(fixture.componentInstance.query.data()).toEqual({ value: 42 })
|
||||
```
|
||||
|
||||
## Handling retries
|
||||
|
||||
Retries slow failing tests because the default backoff runs three times. Set `retry: false` (or a specific number) through `defaultOptions` or per query to keep tests fast. If a query intentionally retries, assert on the final state rather than intermediate counts.
|
||||
|
||||
## HttpClient & network stubs
|
||||
|
||||
Angular's `HttpClientTestingModule` plays nicely with PendingTasks. Register it alongside the Query provider and flush responses through `HttpTestingController`:
|
||||
|
||||
```ts
|
||||
TestBed.configureTestingModule({
|
||||
imports: [HttpClientTestingModule],
|
||||
providers: [provideTanStackQuery(queryClient)],
|
||||
})
|
||||
|
||||
const httpCtrl = TestBed.inject(HttpTestingController)
|
||||
const query = TestBed.runInInjectionContext(() =>
|
||||
injectQuery(() => ({
|
||||
queryKey: ['todos'],
|
||||
queryFn: () => lastValueFrom(TestBed.inject(HttpClient).get('/api/todos')),
|
||||
})),
|
||||
)
|
||||
|
||||
const fixturePromise = TestBed.inject(ApplicationRef).whenStable()
|
||||
httpCtrl.expectOne('/api/todos').flush([{ id: 1 }])
|
||||
await fixturePromise
|
||||
|
||||
expect(query.data()).toEqual([{ id: 1 }])
|
||||
httpCtrl.verify()
|
||||
```
|
||||
|
||||
## Infinite queries & pagination
|
||||
|
||||
Use the same pattern for infinite queries: call `fetchNextPage()`, advance timers if you are faking time, then await stability and assert on `data().pages`.
|
||||
|
||||
```ts
|
||||
const infinite = TestBed.runInInjectionContext(() =>
|
||||
injectInfiniteQuery(() => ({
|
||||
queryKey: ['pages'],
|
||||
queryFn: ({ pageParam = 1 }) => fetchPage(pageParam),
|
||||
getNextPageParam: (last, all) => all.length + 1,
|
||||
})),
|
||||
)
|
||||
|
||||
await appRef.whenStable()
|
||||
expect(infinite.data().pages).toHaveLength(1)
|
||||
|
||||
await infinite.fetchNextPage()
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
await appRef.whenStable()
|
||||
|
||||
expect(infinite.data().pages).toHaveLength(2)
|
||||
```
|
||||
|
||||
## Mutations and optimistic updates
|
||||
|
||||
```ts
|
||||
const mutation = TestBed.runInInjectionContext(() =>
|
||||
injectMutation(() => ({
|
||||
mutationFn: async (input: string) => input.toUpperCase(),
|
||||
})),
|
||||
)
|
||||
|
||||
mutation.mutate('test')
|
||||
|
||||
// Trigger effect
|
||||
TestBed.tick()
|
||||
|
||||
await appRef.whenStable()
|
||||
|
||||
expect(mutation.isSuccess()).toBe(true)
|
||||
expect(mutation.data()).toBe('TEST')
|
||||
```
|
||||
|
||||
## Quick checklist
|
||||
|
||||
- Fresh `QueryClient` per test (and clear it afterwards)
|
||||
- Disable or control retries to avoid timeouts
|
||||
- Advance timers + microtasks before `whenStable()` when using fake timers
|
||||
- Use `HttpClientTestingModule` or your preferred mock to assert network calls
|
||||
- Await `whenStable()` after every `refetch`, `fetchNextPage`, or mutation
|
||||
- Prefer `TestBed.runInInjectionContext` for service tests and `fixture.whenStable()` for component tests
|
||||
39
docs/framework/angular/guides/window-focus-refetching.md
Normal file
39
docs/framework/angular/guides/window-focus-refetching.md
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
id: window-focus-refetching
|
||||
title: Window Focus Refetching
|
||||
ref: docs/framework/react/guides/window-focus-refetching.md
|
||||
replace: { '@tanstack/react-query': '@tanstack/angular-query-experimental' }
|
||||
---
|
||||
|
||||
[//]: # 'Example'
|
||||
|
||||
```ts
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideTanStackQuery(
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false, // default: true
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
[//]: # 'Example'
|
||||
[//]: # 'Example2'
|
||||
|
||||
```ts
|
||||
injectQuery(() => ({
|
||||
queryKey: ['todos'],
|
||||
queryFn: fetchTodos,
|
||||
refetchOnWindowFocus: false,
|
||||
}))
|
||||
```
|
||||
|
||||
[//]: # 'Example2'
|
||||
[//]: # 'ReactNative'
|
||||
[//]: # 'ReactNative'
|
||||
Reference in New Issue
Block a user