Skip to content

feat(angular-query): add mutationOptions #8316

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,10 @@
"label": "Mutations",
"to": "framework/angular/guides/mutations"
},
{
"label": "Mutation Options",
"to": "framework/angular/guides/mutation-options"
},
{
"label": "Query Invalidation",
"to": "framework/angular/guides/query-invalidation"
Expand Down
27 changes: 27 additions & 0 deletions docs/framework/angular/guides/mutation-options.md
Original file line number Diff line number Diff line change
@@ -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#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 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)
},
})
}
}
```
86 changes: 85 additions & 1 deletion docs/framework/angular/typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ replace:
'React Query': 'TanStack Query',
'`success`': '`isSuccess()`',
'function:': 'function.',
'separate function': 'separate function or a service',
}
---

Expand Down Expand Up @@ -170,5 +169,90 @@ computed(() => {
```

[//]: # 'RegisterErrorType'
[//]: # 'TypingQueryOptions'

## Typing Query Options

If you inline query options into `injectQuery`, you'll get automatic type inference. However, you might want to extract the query options into a separate function to share them between `injectQuery` and e.g. `prefetchQuery` or manage them in a service. In that case, you'd lose type inference. To get it back, you can use the `queryOptions` helper:

```ts
@Injectable({
providedIn: 'root',
})
export class QueriesService {
private http = inject(HttpClient)

post(postId: number) {
return queryOptions({
queryKey: ['post', postId],
queryFn: () => {
return lastValueFrom(
this.http.get<Post>(
`https://jsonplaceholder.typicode.com/posts/${postId}`,
),
)
},
})
}
}

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

postId = signal(1)

queries = inject(QueriesService)
optionsSignal = computed(() => this.queries.post(this.postId()))

postQuery = injectQuery(() => this.queries.post(1))
postQuery = injectQuery(() => this.queries.post(this.postId()))

// You can also pass a signal which returns query options
postQuery = injectQuery(this.optionsSignal)

someMethod() {
this.queryClient.prefetchQuery(this.queries.post(23))
}
}
```

Further, the `queryKey` returned from `queryOptions` knows about the `queryFn` associated with it, and we can leverage that type information to make functions like `queryClient.getQueryData` aware of those types as well:

```ts
data = this.queryClient.getQueryData(groupOptions().queryKey)
// ^? data: Post | undefined
```

Without `queryOptions`, the type of data would be unknown, unless we'd pass a type parameter:

```ts
data = queryClient.getQueryData<Post>(['post', 1])
```

## Typing Mutation Options

Similarly to `queryOptions`, you can use `mutationOptions` to extract mutation options into a separate function:

```ts
export class QueriesService {
private 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)
},
})
}
}
```

[//]: # 'TypingQueryOptions'
[//]: # 'Materials'
[//]: # 'Materials'
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { mutationOptions } from '../mutation-options'

describe('mutationOptions', () => {
test('should not allow excess properties', () => {
return mutationOptions({
mutationFn: () => Promise.resolve(5),
mutationKey: ['key'],
// @ts-expect-error this is a good error, because onMutates does not exist!
onMutates: 1000,
})
})

test('should infer types for callbacks', () => {
return mutationOptions({
mutationFn: () => Promise.resolve(5),
mutationKey: ['key'],
onSuccess: (data) => {
expectTypeOf(data).toEqualTypeOf<number>()
},
})
})
})
1 change: 1 addition & 0 deletions packages/angular-query-experimental/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type {
UndefinedInitialDataOptions,
} from './query-options'
export { queryOptions } from './query-options'
export { mutationOptions } from './mutation-options'

export type {
DefinedInitialDataInfiniteOptions,
Expand Down
7 changes: 2 additions & 5 deletions packages/angular-query-experimental/src/inject-mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,8 @@ import { noop, shouldThrowError } from './util'

import { lazyInit } from './util/lazy-init/lazy-init'
import type { DefaultError, MutationObserverResult } from '@tanstack/query-core'
import type {
CreateMutateFunction,
CreateMutationOptions,
CreateMutationResult,
} from './types'
import type { CreateMutateFunction, CreateMutationResult } from './types'
import type { CreateMutationOptions } from './mutation-options'

/**
* Injects a mutation: an imperative function that can be invoked which typically performs server side effects.
Expand Down
60 changes: 60 additions & 0 deletions packages/angular-query-experimental/src/mutation-options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type {
DefaultError,
MutationObserverOptions,
OmitKeyof,
} from '@tanstack/query-core'

/**
* Allows to share and re-use mutation options in a type-safe way.
*
* **Example**
*
* ```ts
* export class QueriesService {
* private 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);
* },
* });
* }
* }
*
* queries = inject(QueriesService)
* idSignal = new Signal(0);
* mutation = injectMutation(() => this.queries.updatePost(this.idSignal()))
*
* mutation.mutate({ title: 'New Title' })
* ```
* @param options - The mutation options.
* @returns Mutation options.
* @public
*/
export function mutationOptions<
TData = unknown,
TError = DefaultError,
TVariables = void,
TContext = unknown,
>(
options: MutationObserverOptions<TData, TError, TVariables, TContext>,
): CreateMutationOptions<TData, TError, TVariables, TContext> {
return options

Check warning on line 46 in packages/angular-query-experimental/src/mutation-options.ts

View check run for this annotation

Codecov / codecov/patch

packages/angular-query-experimental/src/mutation-options.ts#L45-L46

Added lines #L45 - L46 were not covered by tests
}

/**
* @public
*/
export interface CreateMutationOptions<
TData = unknown,
TError = DefaultError,
TVariables = void,
TContext = unknown,
> extends OmitKeyof<
MutationObserverOptions<TData, TError, TVariables, TContext>,
'_defaulted'
> {}
14 changes: 0 additions & 14 deletions packages/angular-query-experimental/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import type {
InfiniteQueryObserverOptions,
InfiniteQueryObserverResult,
MutateFunction,
MutationObserverOptions,
MutationObserverResult,
OmitKeyof,
Override,
Expand Down Expand Up @@ -159,19 +158,6 @@ export type DefinedCreateInfiniteQueryResult<
>,
> = MapToSignals<TDefinedInfiniteQueryObserver>

/**
* @public
*/
export interface CreateMutationOptions<
TData = unknown,
TError = DefaultError,
TVariables = void,
TContext = unknown,
> extends OmitKeyof<
MutationObserverOptions<TData, TError, TVariables, TContext>,
'_defaulted'
> {}

/**
* @public
*/
Expand Down
Loading