BETTER-CONVEX

Mutations

Execute mutations with TanStack Query.

In this guide, we'll explore cRPC mutations. You'll learn to execute mutations with TanStack Query, handle success and error states, implement common patterns like toast notifications, and migrate from vanilla Convex.

Overview

cRPC mutations provide a tRPC-like interface for executing mutations with TanStack Query:

FeatureBenefit
Familiar APIuseMutation from TanStack Query
Rich stateisPending, isError, isSuccess, data, error
Built-in callbacksonSuccess, onError, onMutate, onSettled
Retry supportAutomatic retry configuration

Let's see how it works.

import { useMutation } from '@tanstack/react-query';
import { useCRPC } from '@/lib/convex/crpc';

function CreateUser() {
  const crpc = useCRPC();
  const createUser = useMutation(crpc.user.create.mutationOptions());

  return (
    <button
      disabled={createUser.isPending}
      onClick={() => createUser.mutate({ name: 'John', email: 'john@example.com' })}
    >
      {createUser.isPending ? 'Creating...' : 'Create User'}
    </button>
  );
}

mutationOptions

The mutationOptions method creates options for TanStack Query's useMutation hook. Here's the basic usage:

const crpc = useCRPC();

// Basic usage
const createUser = useMutation(crpc.user.create.mutationOptions());

// With callbacks
const updateUser = useMutation(
  crpc.user.update.mutationOptions({
    onSuccess: (data) => {
      toast.success('Updated successfully');
    },
    onError: (error) => {
      toast.error(error.data?.message ?? 'Update failed');
    },
  })
);

Signature

crpc.path.to.mutation.mutationOptions(
  options?   // TanStack Query mutation options (except mutationFn)
)

Options

All standard TanStack Query mutation options are supported except mutationFn (reserved):

OptionTypeDescription
onSuccess(data, variables, context) => voidCalled on successful mutation
onError(error, variables, context) => voidCalled on error
onMutate(variables) => contextCalled before mutation (for optimistic updates)
onSettled(data, error, variables, context) => voidCalled on completion
retrynumber | booleanRetry failed mutations

Mutation Keys

Get type-safe mutation keys for cache operations:

const crpc = useCRPC();

// Get mutation key
const mutationKey = crpc.user.create.mutationKey();
// => ['convexMutation', 'user:create']

Common Patterns

Now let's look at battle-tested patterns you can copy into your project.

Toast Promise

Use toast.promise for loading/success/error states:

const createProject = useMutation(crpc.project.create.mutationOptions());

const onSubmit = (data: FormData) => {
  toast.promise(createProject.mutateAsync({ title: data.title }), {
    loading: 'Creating project...',
    success: 'Project created!',
    error: (e) => e.data?.message ?? 'Failed to create project',
  });
};

Form Submission

Handle form submission with cleanup:

const updateUser = useMutation(
  crpc.user.update.mutationOptions({
    onSuccess: () => {
      form.reset();
      closeModal();
      toast.success('Profile updated');
    },
    onError: () => {
      toast.error('Update failed');
    },
  })
);

const onSubmit = (data: FormData) => {
  updateUser.mutate(data);
};

Inline Callbacks

Pass callbacks directly to mutate:

const deleteSession = useMutation(crpc.session.delete.mutationOptions());

deleteSession.mutate(
  { id: sessionId },
  {
    onSuccess: () => router.push('/sessions'),
    onError: () => toast.error('Delete failed'),
  }
);

Actions as Mutations

Convex actions can be used as mutations for external API calls:

// Actions work with mutationOptions
const scrapeLink = useMutation(crpc.scraper.scrapeLink.mutationOptions());

useEffect(() => {
  if (url) {
    scrapeLink.mutate({ url });
  }
}, [url]);

// Access mutation state
if (scrapeLink.isPending) return <Spinner />;
if (scrapeLink.data) return <LinkPreview data={scrapeLink.data} />;

Note: Actions don't have real-time subscriptions. Use mutationOptions for one-shot calls to external APIs, or queryOptions if you need query caching.

Migrate from Convex

If you're coming from vanilla Convex, here's what changes.

What stays the same

  • Mutations execute against Convex backend
  • Automatic transaction handling
  • Type-safe arguments and returns

What's new

Before (vanilla Convex):

import { useMutation } from 'convex/react';
import { api } from '@convex/_generated/api';

const createUser = useMutation(api.user.create);

// Execute mutation
await createUser({ name: 'John', email: 'john@example.com' });

After (cRPC):

import { useMutation } from '@tanstack/react-query';
import { useCRPC } from '@/lib/convex/crpc';

const crpc = useCRPC();
const createUser = useMutation(crpc.user.create.mutationOptions());

// Execute mutation
await createUser.mutateAsync({ name: 'John', email: 'john@example.com' });

Key differences:

FeatureDescription
TanStack Query stateisPending, isError, isSuccess, data, error
Built-in callbacksonSuccess, onError, onMutate, onSettled
Retry configurationAutomatic retry support
Mutation keysmutationKey() for cache operations

Next Steps

On this page