Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
d64460c
docs(angular-query): add TypeScript documentation
arnoud-dv Nov 15, 2024
deb1dc5
Improve PendingTasks task cleanup, isRestoring() handling
arnoud-dv Nov 22, 2025
4c82cd6
Ensure unit tests are run using component effect scheduling
arnoud-dv Nov 22, 2025
da89570
Use tracking to fix some subtle bugs
arnoud-dv Nov 22, 2025
4b41009
Fix PendingTasks for offline mode
arnoud-dv Nov 22, 2025
06e6507
Use queueMicrotask instead of notifyManager.batchCalls to improve timing
arnoud-dv Nov 22, 2025
3d5331e
Fix isRestoring() handling
arnoud-dv Nov 23, 2025
e5f6028
add changeset
arnoud-dv Nov 23, 2025
d304fde
Improve tests
arnoud-dv Nov 23, 2025
2969fb6
fix(angular-query): statically split proxy objects to avoid reading i…
benjavicente Dec 18, 2025
9c84cc6
fix(angular-query): start pending task early and don't start on destr…
benjavicente Dec 20, 2025
bc8cd29
tests(angular-query): add pending task ssr render test
benjavicente Dec 20, 2025
acf3f71
fix(angular-query): inject query types and tests
benjavicente Dec 20, 2025
fd1d8fd
chore(angular-query): require angular 19 peer
benjavicente Dec 20, 2025
220c955
fix(angular-query): infer select instead of skip-token like other sig…
benjavicente Dec 21, 2025
82ebd09
docs(angular-query): consistency on examples and small improvements
benjavicente Dec 21, 2025
f85d697
chore(angular-query): fix eslint and knip warnings
benjavicente Dec 21, 2025
10c29e1
chore(angular-query): remove compat pending tasks for angular <19
benjavicente Dec 21, 2025
edbe09e
tests(angular-query): only test supported ts versions of angular
benjavicente Dec 21, 2025
e46337b
tests(angular-query): assert other status narrowing on infinite queries
benjavicente Dec 21, 2025
59dc8f7
docs(tanstack-query): other docs improvements
benjavicente Dec 21, 2025
77fb483
docs(angular-query): remove status type narrowing example
benjavicente Dec 21, 2025
a318081
tests(angular-query): improve optimistic mutation test
benjavicente Dec 22, 2025
d086c9c
fix(angular-query): use signal observer in createBaseQuery
benjavicente Dec 22, 2025
33ce3fa
docs(angular-query): fix typos
benjavicente Dec 27, 2025
9202480
feat(angular-query): inject queries refractor + tests
benjavicente Feb 8, 2026
0c2f8ad
feat(angular-query): update docs and references
benjavicente Feb 8, 2026
55b78eb
feat(angular-query): address coderabbit comments
benjavicente Feb 9, 2026
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
23 changes: 19 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,23 @@ projectsQuery = injectQuery(() => ({
[//]: # 'Example'
[//]: # 'Example2'

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

```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 +33 to +45
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
11 changes: 10 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 @@ -17,11 +22,15 @@ mutation = injectMutation(() => ({
[//]: # 'Example2'

```ts
import { Component, inject } from '@angular/core'
import {
injectMutation,
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
6 changes: 3 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 @@ -83,7 +83,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: 7 additions & 2 deletions 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 @@ -39,12 +42,14 @@ TanStack Query provides `injectQueries`, which you can use to dynamically execut
[//]: # 'Example2'

```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) => {
queries: this.users().map((user) => {
return {
queryKey: ['user', user.id],
queryFn: () => fetchUserById(user.id),
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