@suspensive/react-query-5
Version:
Suspensive interfaces for @tanstack/react-query@5
187 lines (149 loc) • 5.4 kB
Markdown
---
name: mutations
description: >-
Mutation render-prop component and mutationOptions for TanStack Query v5.
Load when adding mutations inside list rows, loops, or conditionals where
useMutation cannot be called, removing per-row wrapper components, or
reusing mutation configs with mutationOptions.
metadata:
type: sub-skill
library: '@suspensive/react-query-5'
library_version: 3.21.2
framework: react
requires: ['react-query']
sources:
- 'toss/suspensive:docs/suspensive.org/src/content/en/docs/react-query/Mutation.mdx'
- 'toss/suspensive:docs/suspensive.org/src/content/en/docs/react-query/mutationOptions.mdx'
- 'toss/suspensive:docs/suspensive.org/src/content/en/docs/react-query/migration/migrate-to-v3.mdx'
---
This skill builds on react-query. Read ../SKILL.md first.
`<Mutation/>` exposes a useMutation result as a render prop so each list row or conditional branch can own its mutation without extracting a wrapper component. The render prop receives the full v5 UseMutationResult (`mutate`, `mutateAsync`, `isPending`, `isError`, `error`, `data`, `reset`).
## Setup
```bash
npm install @suspensive/react-query-5 @tanstack/react-query@5
```
Import `Mutation` from '@suspensive/react-query-5'; import `mutationOptions` from '@tanstack/react-query' (official since TSQ v5; the Suspensive re-export is deprecated).
## Core Patterns
### One mutation per list row
```tsx
'use client'
import { Mutation } from '@suspensive/react-query-5'
import { useSuspenseQuery } from '@tanstack/react-query'
import { api } from '~/api'
export const PostsPage = () => {
const { data: posts } = useSuspenseQuery({
queryKey: ['posts'],
queryFn: () => api.getPosts(),
})
return posts.map((post) => (
<Mutation
key={post.id}
mutationFn={({ content }: { content: string }) => api.editPost({ postId: post.id, content })}
>
{(postMutation) => (
<div>
{postMutation.isPending ? <Spinner /> : null}
<div>{post.content}</div>
<textarea
disabled={postMutation.isPending}
onBlur={(e) => postMutation.mutate({ content: e.target.value })}
/>
</div>
)}
</Mutation>
))
}
```
### Reusable configs with mutationOptions
```tsx
'use client'
import { Mutation } from '@suspensive/react-query-5'
import { mutationOptions, useMutation } from '@tanstack/react-query'
const editPostMutationOptions = (postId: number) =>
mutationOptions({
mutationFn: (content: string) =>
fetch(`/api/posts/${postId}`, {
method: 'PATCH',
body: JSON.stringify({ content }),
}).then((res) => res.json()),
})
// Same options work with the hook...
const useEditPost = (postId: number) => useMutation(editPostMutationOptions(postId))
// ...and spread directly into the component
export const EditPost = ({ postId }: { postId: number }) => (
<Mutation {...editPostMutationOptions(postId)}>
{({ mutate, isPending }) => (
<div>
<p>{isPending ? 'Updating...' : 'Latest updated'}</p>
<textarea onBlur={(e) => mutate(e.target.value)} disabled={isPending} />
</div>
)}
</Mutation>
)
```
### Invalidating queries on success
```tsx
'use client'
import { Mutation } from '@suspensive/react-query-5'
import { useQueryClient } from '@tanstack/react-query'
import { api } from '~/api'
export const DeletePostButton = ({ postId }: { postId: number }) => {
const queryClient = useQueryClient()
return (
<Mutation
mutationFn={() => api.deletePost(postId)}
onSuccess={() => queryClient.invalidateQueries({ queryKey: ['posts'] })}
>
{({ mutate, isPending }) => (
<button disabled={isPending} onClick={() => mutate()}>
{isPending ? 'Deleting...' : 'Delete'}
</button>
)}
</Mutation>
)
}
```
## Common Mistakes
### [MEDIUM] Wrapper component per row just for useMutation
Wrong:
```tsx
{
posts.map((post) => <PostItemWithMutation key={post.id} post={post} />)
}
const PostItemWithMutation = ({ post }: { post: Post }) => {
const deleteMutation = useMutation({ mutationFn: () => api.deletePost(post.id) })
return (
<button disabled={deleteMutation.isPending} onClick={() => deleteMutation.mutate()}>
delete
</button>
)
}
```
Correct:
```tsx
{
posts.map((post) => (
<Mutation key={post.id} mutationFn={() => api.deletePost(post.id)}>
{({ mutate, isPending }) => (
<button disabled={isPending} onClick={() => mutate()}>
delete
</button>
)}
</Mutation>
))
}
```
Hooks cannot be called in loops, so agents invent awkwardly named wrapper components; `<Mutation>` works inline per row with no extra depth.
Source: docs/suspensive.org/src/content/en/docs/react-query/Mutation.mdx
### [MEDIUM] Importing mutationOptions from Suspensive
Wrong:
```tsx
import { mutationOptions } from '@suspensive/react-query-5'
```
Correct:
```tsx
import { mutationOptions } from '@tanstack/react-query'
```
mutationOptions is official in TanStack Query (v5, backported to v4.44+); the Suspensive re-export exists only for compatibility and is deprecated.
Source: docs/suspensive.org/src/content/en/docs/react-query/mutationOptions.mdx
See also: ../declarative-queries/SKILL.md for the matching SuspenseQuery pattern, ../SKILL.md for which imports come from @tanstack/react-query vs Suspensive.