UNPKG

@suspensive/react-query-4

Version:

Suspensive interfaces for @tanstack/react-query@4

216 lines (172 loc) 6.26 kB
--- name: mutations description: >- Mutation render-prop component and mutationOptions for TanStack Query v4. 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-4' 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 v4 UseMutationResult (`mutate`, `mutateAsync`, `isLoading`, `isError`, `error`, `data`, `reset`). Note the v4 naming: the in-flight flag is `isLoading`, not v5's `isPending`. ## Setup ```bash npm install @suspensive/react-query-4 @tanstack/react-query@4 ``` Import `Mutation` from '@suspensive/react-query-4'; import `mutationOptions` from '@tanstack/react-query' (official since TSQ 4.44+, which the package's peer range requires; the Suspensive re-export is deprecated). ## Core Patterns ### One mutation per list row ```tsx 'use client' import { Mutation } from '@suspensive/react-query-4' 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.isLoading ? <Spinner /> : null} <div>{post.content}</div> <textarea disabled={postMutation.isLoading} onBlur={(e) => postMutation.mutate({ content: e.target.value })} /> </div> )} </Mutation> )) } ``` ### Reusable configs with mutationOptions ```tsx 'use client' import { Mutation } from '@suspensive/react-query-4' 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, isLoading }) => ( <div> <p>{isLoading ? 'Updating...' : 'Latest updated'}</p> <textarea onBlur={(e) => mutate(e.target.value)} disabled={isLoading} /> </div> )} </Mutation> ) ``` ### Invalidating queries on success ```tsx 'use client' import { Mutation } from '@suspensive/react-query-4' 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, isLoading }) => ( <button disabled={isLoading} onClick={() => mutate()}> {isLoading ? '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.isLoading} onClick={() => deleteMutation.mutate()}> delete </button> ) } ``` Correct: ```tsx { posts.map((post) => ( <Mutation key={post.id} mutationFn={() => api.deletePost(post.id)}> {({ mutate, isLoading }) => ( <button disabled={isLoading} 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] Using v5 isPending on v4 mutation results Wrong: ```tsx <Mutation mutationFn={() => api.deletePost(postId)}> {({ mutate, isPending }) => ( <button disabled={isPending} onClick={() => mutate()}> delete </button> )} </Mutation> ``` Correct: ```tsx <Mutation mutationFn={() => api.deletePost(postId)}> {({ mutate, isLoading }) => ( <button disabled={isLoading} onClick={() => mutate()}> delete </button> )} </Mutation> ``` The render prop receives TSQ v4's UseMutationResult, where the in-flight flag is `isLoading`; `isPending` only exists in TSQ v5 (and @suspensive/react-query-5). 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-4' ``` Correct: ```tsx import { mutationOptions } from '@tanstack/react-query' ``` mutationOptions is official in TanStack Query since v4.44 (the package's peer range requires ^4.44.0); 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.