Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
4579364
docs(angular-query): add TypeScript documentation
arnoud-dv Nov 15, 2024
7951412
Improve PendingTasks task cleanup, isRestoring() handling
arnoud-dv Nov 22, 2025
d7e8d66
Ensure unit tests are run using component effect scheduling
arnoud-dv Nov 22, 2025
0edde05
Use tracking to fix some subtle bugs
arnoud-dv Nov 22, 2025
1438a89
Fix PendingTasks for offline mode
arnoud-dv Nov 22, 2025
e9439a5
Use queueMicrotask instead of notifyManager.batchCalls to improve timing
arnoud-dv Nov 22, 2025
2df8983
Fix isRestoring() handling
arnoud-dv Nov 23, 2025
a9adba6
add changeset
arnoud-dv Nov 23, 2025
ffb096e
Improve tests
arnoud-dv Nov 23, 2025
c1bc243
fix(angular-query): statically split proxy objects to avoid reading i…
benjavicente Dec 18, 2025
c727bc7
fix(angular-query): start pending task early and don't start on destr…
benjavicente Dec 20, 2025
060f9ac
tests(angular-query): add pending task ssr render test
benjavicente Dec 20, 2025
e7bb141
fix(angular-query): inject query types and tests
benjavicente Dec 20, 2025
800311b
chore(angular-query): require angular 19 peer
benjavicente Dec 20, 2025
0fd6dbd
fix(angular-query): infer select instead of skip-token like other sig…
benjavicente Dec 21, 2025
1dad967
docs(angular-query): consistency on examples and small improvements
benjavicente Dec 21, 2025
b31faf7
chore(angular-query): fix eslint and knip warnings
benjavicente Dec 21, 2025
608f898
chore(angular-query): remove compat pending tasks for angular <19
benjavicente Dec 21, 2025
f768ce5
tests(angular-query): only test supported ts versions of angular
benjavicente Dec 21, 2025
0b9b1a7
tests(angular-query): assert other status narrowing on infinite queries
benjavicente Dec 21, 2025
85c37fd
docs(tanstack-query): other docs improvements
benjavicente Dec 21, 2025
03f8a7d
docs(angular-query): remove status type narrowing example
benjavicente Dec 21, 2025
3528b4b
tests(angular-query): improve optimistic mutation test
benjavicente Dec 22, 2025
00ba5b4
fix(angular-query): use signal observer in createBaseQuery
benjavicente Dec 22, 2025
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
5 changes: 5 additions & 0 deletions .changeset/deep-crews-open.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/angular-query-experimental': minor
---

require Angular v19+ and use Angular component effect scheduling
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,6 @@ class ExampleComponent {
```

> Since Angular is moving towards RxJS as an optional dependency, it's expected that `HttpClient` will also support promises in the future.
>
> Support for observables in TanStack Query for Angular is planned.

## Comparison table

Expand Down
2 changes: 1 addition & 1 deletion docs/framework/angular/guides/caching.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Let's assume we are using the default `gcTime` of **5 minutes** and the default
- 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'], queyFn: 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.
- 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.
Expand Down
16 changes: 12 additions & 4 deletions docs/framework/angular/guides/default-query-function.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,27 @@ bootstrapApplication(MyAppComponent, {
providers: [provideTanStackQuery(queryClient)],
})

export class PostsComponent {
@Component({
// ...
})
class PostsComponent {
// All you have to do now is pass a key!
postsQuery = injectQuery<Array<Post>>(() => ({
queryKey: ['/posts'],
}))
// ...
}

export class PostComponent {
@Component({
// ...
})
class PostComponent {
postId = input(0)

// You can even leave out the queryFn and just go straight into options
postQuery = injectQuery<Post>(() => ({
enabled: this.postIdSignal() > 0,
queryKey: [`/posts/${this.postIdSignal()}`],
enabled: this.postId() > 0,
queryKey: [`/posts/${this.postId()}`],
}))
// ...
}
Expand Down
25 changes: 21 additions & 4 deletions docs/framework/angular/guides/dependent-queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ replace: { 'useQuery': 'injectQuery', 'useQueries': 'injectQueries' }
```ts
// Get the user
userQuery = injectQuery(() => ({
queryKey: ['user', email],
queryFn: getUserByEmail,
queryKey: ['user', this.email()],
queryFn: this.getUserByEmail,
}))

// Then get the user's projects
projectsQuery = injectQuery(() => ({
queryKey: ['projects', this.userQuery.data()?.id],
queryFn: getProjectsByUser,
queryFn: this.getProjectsByUser,
// The query will not execute until the user id exists
enabled: !!this.userQuery.data()?.id,
}))
Expand All @@ -26,8 +26,25 @@ projectsQuery = injectQuery(() => ({
[//]: # 'Example'
[//]: # 'Example2'

Dynamic parallel query - `injectQueries` can depend on a previous query also, here's how to achieve this:

> IMPORTANT: `injectQueries` is experimental and is provided in it's own entry point

```ts
// injectQueries is under development for Angular Query
// Get the users ids
userIds = injectQuery(() => ({
queryKey: ['users'],
queryFn: getUserData,
select: (users) => users.map((user) => user.id),
}))

// Then get the users messages
userQueries = injectQueries(() => ({
queries: (this.userIds() ?? []).map((userId) => ({
queryKey: ['user', userId],
queryFn: () => getUserById(userId),
})),
}))
Comment on lines +35 to +47
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Incorrect data access pattern in injectQueries example.

Line 43 uses this.userIds() but userIds is the query result object from injectQuery, not a signal of the data. To access the selected user IDs, it should be this.userIds.data().

🔎 Proposed fix
 // Then get the users messages
 userQueries = injectQueries(() => ({
-  queries: (this.userIds() ?? []).map((userId) => ({
+  queries: (this.userIds.data() ?? []).map((userId) => ({
     queryKey: ['user', userId],
     queryFn: () => getUserById(userId),
   })),
 }))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
userIds = injectQuery(() => ({
queryKey: ['users'],
queryFn: getUserData,
select: (users) => users.map((user) => user.id),
}))
// Then get the users messages
userQueries = injectQueries(() => ({
queries: (this.userIds() ?? []).map((userId) => ({
queryKey: ['user', userId],
queryFn: () => getUserById(userId),
})),
}))
userIds = injectQuery(() => ({
queryKey: ['users'],
queryFn: getUserData,
select: (users) => users.map((user) => user.id),
}))
// Then get the users messages
userQueries = injectQueries(() => ({
queries: (this.userIds.data() ?? []).map((userId) => ({
queryKey: ['user', userId],
queryFn: () => getUserById(userId),
})),
}))
🤖 Prompt for AI Agents
In docs/framework/angular/guides/dependent-queries.md around lines 35 to 47, the
injectQueries example incorrectly calls this.userIds() (userIds is a query
result object), so update the code to read the selected IDs from the query
result by using this.userIds.data() when mapping; ensure you also keep a safe
fallback like (this.userIds.data() ?? []) so the map runs on an array.

```

[//]: # 'Example2'
6 changes: 3 additions & 3 deletions docs/framework/angular/guides/disabling-queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ replace: { 'useQuery': 'injectQuery' }
template: `<div>
<button (click)="query.refetch()">Fetch Todos</button>

@if (query.data()) {
@if (query.data(); as data) {
<ul>
@for (todo of query.data(); track todo.id) {
@for (todo of data; track todo.id) {
<li>{{ todo.title }}</li>
}
</ul>
Expand Down Expand Up @@ -70,7 +70,7 @@ export class TodosComponent {
[//]: # 'Example3'

```angular-ts
import { skipToken, injectQuery } from '@tanstack/query-angular'
import { skipToken, injectQuery } from '@tanstack/angular-query-experimental'

@Component({
selector: 'todos',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,7 @@ replace:
'useQuery': 'injectQuery',
'useMutation': 'injectMutation',
'hook': 'function',
'Redux, MobX or': 'NgRx Store or',
'Redux, MobX, Zustand': 'NgRx Store, custom services with RxJS',
}
---
1 change: 0 additions & 1 deletion docs/framework/angular/guides/important-defaults.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ title: Important Defaults
ref: docs/framework/react/guides/important-defaults.md
replace:
{
'React': 'Angular',
'react-query': 'angular-query',
'useQuery': 'injectQuery',
'useInfiniteQuery': 'injectInfiniteQuery',
Expand Down
2 changes: 2 additions & 0 deletions docs/framework/angular/guides/infinite-queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ export class Example {
template: ` <list-component (endReached)="fetchNextPage()" /> `,
})
export class Example {
projectsService = inject(ProjectsService)
query = injectInfiniteQuery(() => ({
queryKey: ['projects'],
queryFn: async ({ pageParam }) => {
Expand Down
10 changes: 6 additions & 4 deletions docs/framework/angular/guides/initial-query-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ result = injectQuery(() => ({
```ts
result = injectQuery(() => ({
queryKey: ['todo', this.todoId()],
queryFn: () => fetch('/todos'),
queryFn: () => fetch(`/todos/${this.todoId()}`),
initialData: () => {
// Use a todo from the 'todos' query as the initial data for this todo query
return this.queryClient
Expand All @@ -99,9 +99,11 @@ result = injectQuery(() => ({
queryKey: ['todos', this.todoId()],
queryFn: () => fetch(`/todos/${this.todoId()}`),
initialData: () =>
queryClient.getQueryData(['todos'])?.find((d) => d.id === this.todoId()),
this.queryClient
.getQueryData(['todos'])
?.find((d) => d.id === this.todoId()),
initialDataUpdatedAt: () =>
queryClient.getQueryState(['todos'])?.dataUpdatedAt,
this.queryClient.getQueryState(['todos'])?.dataUpdatedAt,
}))
```

Expand All @@ -114,7 +116,7 @@ result = injectQuery(() => ({
queryFn: () => fetch(`/todos/${this.todoId()}`),
initialData: () => {
// Get the query state
const state = queryClient.getQueryState(['todos'])
const state = this.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) {
Expand Down
10 changes: 9 additions & 1 deletion docs/framework/angular/guides/invalidations-from-mutations.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@
id: invalidations-from-mutations
title: Invalidations from Mutations
ref: docs/framework/react/guides/invalidations-from-mutations.md
replace: { 'useMutation': 'injectMutation', 'hook': 'function' }
replace:
{
'react-query': 'angular-query-experimental',
'useMutation': 'injectMutation',
'hook': 'function',
}
---

[//]: # 'Example'
Expand All @@ -22,6 +27,9 @@ import {
QueryClient,
} from '@tanstack/angular-query-experimental'

@Component({
// ...
})
export class TodosComponent {
queryClient = inject(QueryClient)

Expand Down
7 changes: 5 additions & 2 deletions docs/framework/angular/guides/mutations.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,12 +243,15 @@ queryClient.setMutationDefaults(['addTodo'], {
retry: 3,
})

class someComponent {
@Component({
// ...
})
class SomeComponent {
// Start mutation in some component:
mutation = injectMutation(() => ({ mutationKey: ['addTodo'] }))

someMethod() {
mutation.mutate({ title: 'title' })
this.mutation.mutate({ title: 'title' })
}
}

Expand Down
10 changes: 7 additions & 3 deletions docs/framework/angular/guides/paginated-queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ const result = injectQuery(() => ({
instantaneously while they are also re-fetched invisibly in the
background.
</p>
@if (query.status() === 'pending') {
@if (query.isPending()) {
<div>Loading...</div>
} @else if (query.status() === 'error') {
} @else if (query.isError()) {
<div>Error: {{ query.error().message }}</div>
} @else {
<!-- 'data' will either resolve to the latest page's data -->
Expand Down Expand Up @@ -68,6 +68,10 @@ const result = injectQuery(() => ({
</div>
`,
})

@Component({
// ...
})
export class PaginationExampleComponent {
Comment on lines +71 to 75
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Duplicate @Component decorator appears to be unintentional.

There's already a complete @Component decorator with the full template at lines 26-70. This second, empty @Component({ // ... }) block creates invalid Angular code since a class cannot have two decorators of the same type.

🔎 Proposed fix
     </div>
   `,
 })
-
-@Component({
-  // ...
-})
 export class PaginationExampleComponent {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Component({
// ...
})
export class PaginationExampleComponent {
`,
})
export class PaginationExampleComponent {
🤖 Prompt for AI Agents
In docs/framework/angular/guides/paginated-queries.md around lines 71 to 75
there is a duplicate, empty @Component decorator applied to
PaginationExampleComponent which is invalid; remove the second @Component({ //
... }) block so the class only has the original complete @Component decorator
(lines ~26-70) and ensure the class definition follows that single decorator.

page = signal(0)
queryClient = inject(QueryClient)
Expand All @@ -83,7 +87,7 @@ export class PaginationExampleComponent {
effect(() => {
// Prefetch the next page!
if (!this.query.isPlaceholderData() && this.query.data()?.hasMore) {
this.#queryClient.prefetchQuery({
this.queryClient.prefetchQuery({
queryKey: ['projects', this.page() + 1],
queryFn: () => lastValueFrom(fetchProjects(this.page() + 1)),
})
Expand Down
9 changes: 8 additions & 1 deletion docs/framework/angular/guides/parallel-queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ replace:
[//]: # 'Example'

```ts
@Component({
// ...
})
export class AppComponent {
// The following queries will execute in parallel
usersQuery = injectQuery(() => ({ queryKey: ['users'], queryFn: fetchUsers }))
Expand All @@ -38,11 +41,15 @@ TanStack Query provides `injectQueries`, which you can use to dynamically execut
[//]: # 'DynamicParallelIntro'
[//]: # 'Example2'

> IMPORTANT: `injectQueries` is experimental and is provided in it's own entry point
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Minor grammatical correction needed.

Change "it's own entry point" to "its own entry point" (possessive form, not contraction).

🔎 Proposed fix
-> IMPORTANT: `injectQueries` is experimental and is provided in it's own entry point
+> IMPORTANT: `injectQueries` is experimental and is provided in its own entry point
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
> IMPORTANT: `injectQueries` is experimental and is provided in it's own entry point
> IMPORTANT: `injectQueries` is experimental and is provided in its own entry point
🤖 Prompt for AI Agents
In docs/framework/angular/guides/parallel-queries.md at line 44, the phrase
"it's own entry point" uses the contraction "it's" incorrectly; change it to the
possessive "its own entry point" to correct the grammar.


```ts
@Component({
// ...
})
export class AppComponent {
users = signal<Array<User>>([])

// Please note injectQueries is under development and this code does not work yet
userQueries = injectQueries(() => ({
queries: users().map((user) => {
return {
Expand Down
17 changes: 14 additions & 3 deletions docs/framework/angular/guides/placeholder-query-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ ref: docs/framework/react/guides/placeholder-query-data.md
[//]: # 'ExampleValue'

```ts
@Component({
// ...
})
class TodosComponent {
result = injectQuery(() => ({
queryKey: ['todos'],
Expand All @@ -22,10 +25,15 @@ class TodosComponent {
[//]: # 'ExampleFunction'

```ts
class TodosComponent {
@Component({
// ...
})
export class TodosComponent {
todoId = signal(1)

result = injectQuery(() => ({
queryKey: ['todos', id()],
queryFn: () => fetch(`/todos/${id}`),
queryKey: ['todos', this.todoId()],
queryFn: () => fetch(`/todos/${this.todoId()}`),
placeholderData: (previousData, previousQuery) => previousData,
}))
}
Expand All @@ -35,6 +43,9 @@ class TodosComponent {
[//]: # 'ExampleCache'

```ts
@Component({
// ...
})
export class BlogPostComponent {
postId = input.required<number>()
queryClient = inject(QueryClient)
Expand Down
34 changes: 3 additions & 31 deletions docs/framework/angular/guides/queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ replace:
```ts
import { injectQuery } from '@tanstack/angular-query-experimental'

@Component({
// ...
})
export class TodosComponent {
info = injectQuery(() => ({ queryKey: ['todos'], queryFn: fetchTodoList }))
}
Expand Down Expand Up @@ -61,38 +64,7 @@ export class PostsComponent {
```

[//]: # 'Example3'

If booleans aren't your thing, you can always use the `status` state as well:

[//]: # 'Example4'

```angular-ts
@Component({
selector: 'todos',
template: `
@switch (todos.status()) {
@case ('pending') {
<span>Loading...</span>
}
@case ('error') {
<span>Error: {{ todos.error()?.message }}</span>
}
<!-- also status === 'success', but "else" logic works, too -->
@default {
<ul>
@for (todo of todos.data(); track todo.id) {
<li>{{ todo.title }}</li>
} @empty {
<li>No todos found</li>
}
</ul>
}
}
`,
})
class TodosComponent {}
```

[//]: # 'Example4'
[//]: # 'Materials'
[//]: # 'Materials'
Loading