Teardown

Mutations

Create, update, delete operations and cache invalidation.

Creating Mutations

Use the mutation() method for write operations:

class ProjectsClient extends QueryClient<"projects"> {
  createProject() {
    return this.mutation<
      "create",                    // Mutation key
      Project,                     // Return type
      ApiError,                    // Error type
      { name: string; slug: string } // Variables type
    >({
      key: ["create"],
      fn: async (data) => {
        const response = await fetch("/api/projects", {
          method: "POST",
          body: JSON.stringify(data),
        });
        return response.json();
      },
      onSuccess: () => {
        this.refresh("list"); // Invalidate list queries
      },
    });
  }
}

Using Mutations

import { useMutation } from "@tanstack/react-query";

function CreateProjectForm() {
  const projectsClient = useProjectsClient();
  const { mutate, isPending, error } = useMutation(projectsClient.createProject());

  const handleSubmit = (formData) => {
    mutate(formData, {
      onSuccess: (newProject) => {
        // Navigate to new project
        router.push(`/projects/${newProject.id}`);
      },
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="name" placeholder="Project name" />
      <input name="slug" placeholder="project-slug" />
      <button type="submit" disabled={isPending}>
        {isPending ? "Creating..." : "Create Project"}
      </button>
      {error && <p className="error">{error.message}</p>}
    </form>
  );
}

Cache Invalidation

Invalidate related queries after mutations to keep data fresh:

class ProjectsClient extends QueryClient<"projects"> {
  updateProject(projectId: string) {
    return this.mutation({
      key: ["update", projectId],
      fn: async (data: UpdateProjectInput) => {
        const response = await fetch(`/api/projects/${projectId}`, {
          method: "PATCH",
          body: JSON.stringify(data),
        });
        return response.json();
      },
      onSuccess: () => {
        // Invalidate both list and detail queries
        this.refresh("list");
        this.refresh("byId", projectId);
      },
    });
  }

  deleteProject(projectId: string) {
    return this.mutation({
      key: ["delete", projectId],
      fn: async () => {
        await fetch(`/api/projects/${projectId}`, { method: "DELETE" });
      },
      onSuccess: () => {
        // Invalidate all project queries
        this.refresh();
      },
    });
  }
}

Optimistic Updates

Update the cache before the mutation completes:

updateProject(projectId: string) {
  return this.mutation({
    key: ["update", projectId],
    fn: updateProjectApi,
    onMutate: async (newData) => {
      // Cancel outgoing refetches
      await queryClient.cancelQueries({ queryKey: ["@teardown", "projects"] });

      // Snapshot previous value
      const previous = this.getProject(projectId).get();

      // Optimistically update
      this.getProject(projectId).set({ ...previous, ...newData });

      return { previous };
    },
    onError: (_err, _vars, context) => {
      // Rollback on error
      if (context?.previous) {
        this.getProject(projectId).set(context.previous);
      }
    },
    onSettled: () => {
      // Refetch after mutation
      this.refresh("byId", projectId);
    },
  });
}

Error Handling

Handle errors consistently in mutations:

deleteProject(projectId: string) {
  return this.mutation({
    key: ["delete", projectId],
    fn: async () => {
      const response = await fetch(`/api/projects/${projectId}`, {
        method: "DELETE",
      });
      if (!response.ok) {
        const error = await response.json();
        throw error;
      }
    },
    onError: (error) => {
      if (error.status === 404) {
        // Handle not found
        toast.error("Project not found");
      } else if (error.status === 403) {
        // Handle forbidden
        toast.error("You don't have permission to delete this project");
      } else {
        toast.error("Failed to delete project");
      }
    },
  });
}

Mutation Callbacks

All TanStack Query mutation callbacks are supported:

this.mutation({
  key: ["create"],
  fn: createProjectFn,

  // Before mutation starts
  onMutate: async (variables) => {
    // Return context for rollback
    return { previousData };
  },

  // On success
  onSuccess: (data, variables, context) => {
    // Update cache, show toast, navigate
  },

  // On error
  onError: (error, variables, context) => {
    // Rollback, show error
  },

  // Always runs (success or error)
  onSettled: (data, error, variables, context) => {
    // Cleanup, refetch
  },
});

Mutation Result

interface CreateMutationResult<Resource, MKey, TData, TError, TVariables, TContext> {
  mutationKey: MutationKey<Resource, MKey>;
  mutationFn: MutationFunction<TData, TVariables>;
  // ... other MutationOptions
}

Cross-Resource Invalidation

Invalidate queries across multiple resources:

class ProjectsClient extends QueryClient<"projects"> {
  constructor(
    tanstackClient: TanstackQueryClient,
    private readonly orgsClient: OrgsClient
  ) {
    super(tanstackClient, "projects");
  }

  deleteProject(projectId: string) {
    return this.mutation({
      key: ["delete", projectId],
      fn: deleteProjectFn,
      onSuccess: () => {
        // Invalidate project queries
        this.refresh();
        // Also invalidate org stats (cross-resource)
        this.orgsClient.refresh("stats");
      },
    });
  }
}