Implement a create flow with pending, error, and success states.
Protect the server boundary first
Client types do not replace runtime validation. A POST Server Function uses validator before the handler to turn unknown data into trusted input; real apps should also authenticate and authorize here.
export const createIssue = createServerFn({ method: 'POST' })
.validator((input: { title: string }) => {
const title = input.title.trim()
if (title.length < 3) throw new Error('Title is too short')
return { title }
})
.handler(async ({ data }) => saveIssue(data))Update the single source of truth from the response
useMutation owns the write lifecycle. On success, update cache from the complete server response; return a new array instead of pushing into the old cached value.
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: (input: CreateIssueInput) =>
createIssue({ data: input }),
onSuccess: (issue) => {
queryClient.setQueryData<Issue[]>(['issues'], (current = []) => [
issue,
...current,
])
},
})Turn this lesson into a verifiable skill
List and detail caches agree after a successful write, while failed writes do not leave unconfirmed data visible.
After creating an issue, seed its detail cache and invalidate only the affected list prefix.
Exercise success and failure; success should seed detail data, while failure should preserve the previous cache value.
- Invalidating the entire query cache after every mutation.
- Assuming the server response is identical to the submitted input.