Skip to content
Open
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
1 change: 1 addition & 0 deletions docs/.vitepress/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export default defineConfig({
{ text: "useSuspenseQuery", link: "/use-suspense-query" },
{ text: "useInfiniteQuery", link: "/use-infinite-query" },
{ text: "queryOptions", link: "/query-options" },
{ text: "mutationOptions", link: "/mutation-options" },
],
},
{
Expand Down
88 changes: 88 additions & 0 deletions docs/openapi-react-query/mutation-options.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
---
title: mutationOptions
---

# {{ $frontmatter.title }}

The `mutationOptions` method lets you build type-safe [Mutation Options](https://tanstack.com/query/latest/docs/framework/react/reference/mutationOptions) that plug directly into React Query APIs.

Use it whenever you want to call `$api` mutations but still provide your own `useMutation` (or other mutation consumer) configuration. The helper wires up the correct `mutationKey`, generates a fetcher that calls your OpenAPI endpoint, and preserves the inferred `data` and `error` types.

## Examples

Rewriting the [useMutation example](use-mutation#example) to use `mutationOptions`.

::: code-group

```tsx [src/app.tsx]
import { useMutation } from '@tanstack/react-query';
import { $api } from './api';

export const App = () => {
const updateUser = useMutation(
$api.mutationOptions('patch', '/users/{user_id}', {
onSuccess: (data, variables) => {
console.log('Updated', variables.params?.path?.user_id, data.firstname);
},
})
);

return (
<button
onClick={() =>
updateUser.mutate({
params: { path: { user_id: 5 } },
body: { firstname: 'John' },
})
}
>
Update
</button>
);
};
```

```ts [src/api.ts]
import createFetchClient from 'openapi-fetch';
import createClient from 'openapi-react-query';
import type { paths } from './my-openapi-3-schema'; // generated by openapi-typescript

const fetchClient = createFetchClient<paths>({
baseUrl: 'https://myapi.dev/v1/',
});
export const $api = createClient(fetchClient);
```

:::

::: info Good to Know

`$api.useMutation` uses the same fetcher and key contract as `mutationOptions`. Reach for `mutationOptions` when you need to share mutation configuration across components or call React Query utilities such as `queryClient.mutationDefaults`.

:::

## API

```tsx
const options = $api.mutationOptions(method, path, mutationOptions);
```

**Arguments**

- `method` **(required)**
- HTTP method of the OpenAPI operation.
- Also used as the first element of the mutation key.
- `path` **(required)**
- Pathname of the OpenAPI operation.
- Must be valid for the given method in your generated schema.
- Used as the second element of the mutation key.
- `mutationOptions`
- Optional `UseMutationOptions` for React Query.
- You can set callbacks (`onSuccess`, `onSettled`, …), retry behaviour, and every option except `mutationKey` and `mutationFn` (those are provided for you).

**Returns**

- [Mutation Options](https://tanstack.com/query/latest/docs/framework/react/reference/mutationOptions)
- `mutationKey` is `[method, path]`.
- `mutationFn` is a strongly typed fetcher that calls `openapi-fetch` with your `init` payload.
- `data` and `error` types match the OpenAPI schema, so `variables` inside callbacks are typed as the request shape.
106 changes: 96 additions & 10 deletions packages/openapi-react-query/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ export type QueryKey<
Init = MaybeOptionalInit<Paths[Path], Method>,
> = Init extends undefined ? readonly [Method, Path] : readonly [Method, Path, Init];

export type MutationKey<Method extends HttpMethod, Path> = readonly [Method, Path];

export type QueryOptionsFunction<Paths extends Record<string, Record<HttpMethod, {}>>, Media extends MediaType> = <
Method extends HttpMethod,
Path extends PathsWithMethod<Paths, Method>,
Expand Down Expand Up @@ -79,6 +81,75 @@ export type QueryOptionsFunction<Paths extends Record<string, Record<HttpMethod,
}
>;

export type MutationOptionsFunction<Paths extends Record<string, Record<HttpMethod, {}>>, Media extends MediaType> = <
Method extends HttpMethod,
Path extends PathsWithMethod<Paths, Method>,
Init extends MaybeOptionalInit<Paths[Path], Method>,
Response extends Required<FetchResponse<Paths[Path][Method], Init, Media>>,
Options extends Omit<UseMutationOptions<Response["data"], Response["error"], Init>, "mutationKey" | "mutationFn">,
>(
method: Method,
path: Path,
options?: Options,
) => NoInfer<
Omit<UseMutationOptions<Response["data"], Response["error"], Init>, "mutationFn"> & {
mutationFn: Exclude<UseMutationOptions<Response["data"], Response["error"], Init>["mutationFn"], undefined>;
}
>;

// Helper type to infer TPageParam type
type InferPageParamType<T> = T extends { initialPageParam: infer P } ? P : unknown;

export type InfiniteQueryOptionsFunction<
Paths extends Record<string, Record<HttpMethod, {}>>,
Media extends MediaType,
> = <
Method extends HttpMethod,
Path extends PathsWithMethod<Paths, Method>,
Init extends MaybeOptionalInit<Paths[Path], Method>,
Response extends Required<FetchResponse<Paths[Path][Method], Init, Media>>,
Options extends Omit<
UseInfiniteQueryOptions<
Response["data"],
Response["error"],
InferSelectReturnType<InfiniteData<Response["data"]>, Options["select"]>,
QueryKey<Paths, Method, Path>,
InferPageParamType<Options>
>,
"queryKey" | "queryFn"
> & {
pageParamName?: string;
initialPageParam: InferPageParamType<Options>;
},
>(
method: Method,
path: Path,
init: InitWithUnknowns<Init>,
options: Options,
) => NoInfer<
Omit<
UseInfiniteQueryOptions<
Response["data"],
Response["error"],
InferSelectReturnType<InfiniteData<Response["data"]>, Options["select"]>,
QueryKey<Paths, Method, Path>,
InferPageParamType<Options>
>,
"queryFn"
> & {
queryFn: Exclude<
UseInfiniteQueryOptions<
Response["data"],
Response["error"],
InferSelectReturnType<InfiniteData<Response["data"]>, Options["select"]>,
QueryKey<Paths, Method, Path>,
InferPageParamType<Options>
>["queryFn"],
SkipToken | undefined
>;
}
>;

export type UseQueryMethod<Paths extends Record<string, Record<HttpMethod, {}>>, Media extends MediaType> = <
Method extends HttpMethod,
Path extends PathsWithMethod<Paths, Method>,
Expand Down Expand Up @@ -166,6 +237,7 @@ export type UseMutationMethod<Paths extends Record<string, Record<HttpMethod, {}

export interface OpenapiQueryClient<Paths extends {}, Media extends MediaType = MediaType> {
queryOptions: QueryOptionsFunction<Paths, Media>;
mutationOptions: MutationOptionsFunction<Paths, Media>;
useQuery: UseQueryMethod<Paths, Media>;
useSuspenseQuery: UseSuspenseQueryMethod<Paths, Media>;
useInfiniteQuery: UseInfiniteQueryMethod<Paths, Media>;
Expand Down Expand Up @@ -204,6 +276,22 @@ export default function createClient<Paths extends {}, Media extends MediaType =
return data;
};

const createMutationFn = <Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>>(
method: Method,
path: Path,
) => {
return async (init: any) => {
const mth = method.toUpperCase() as Uppercase<typeof method>;
const fn = client[mth] as ClientMethod<Paths, typeof method, Media>;
const { data, error } = await fn(path, init as any);
if (error) {
throw error;
}

return data as Exclude<typeof data, undefined>;
};
};

const queryOptions: QueryOptionsFunction<Paths, Media> = (method, path, ...[init, options]) => ({
queryKey: (init === undefined ? ([method, path] as const) : ([method, path, init] as const)) as QueryKey<
Paths,
Expand All @@ -214,8 +302,15 @@ export default function createClient<Paths extends {}, Media extends MediaType =
...options,
});

const mutationOptions: MutationOptionsFunction<Paths, Media> = (method, path, options) => ({
mutationKey: [method, path] as MutationKey<typeof method, typeof path>,
mutationFn: createMutationFn(method, path),
...options,
});

return {
queryOptions,
mutationOptions,
useQuery: (method, path, ...[init, options, queryClient]) =>
useQuery(queryOptions(method, path, init as InitWithUnknowns<typeof init>, options), queryClient),
useSuspenseQuery: (method, path, ...[init, options, queryClient]) =>
Expand Down Expand Up @@ -256,16 +351,7 @@ export default function createClient<Paths extends {}, Media extends MediaType =
useMutation(
{
mutationKey: [method, path],
mutationFn: async (init) => {
const mth = method.toUpperCase() as Uppercase<typeof method>;
const fn = client[mth] as ClientMethod<Paths, typeof method, Media>;
const { data, error } = await fn(path, init as InitWithUnknowns<typeof init>);
if (error) {
throw error;
}

return data as Exclude<typeof data, undefined>;
},
mutationFn: createMutationFn(method, path),
...options,
},
queryClient,
Expand Down
Loading