diff --git a/packages/react/src/firestore/index.ts b/packages/react/src/firestore/index.ts
index 717bdbb9..eb8be198 100644
--- a/packages/react/src/firestore/index.ts
+++ b/packages/react/src/firestore/index.ts
@@ -2,7 +2,7 @@
// useEnableIndexedDbPersistenceMutation
// useDisableNetworkMutation
// useEnableNetworkMutation
-// useRunTransactionMutation
+export { useRunTransactionMutation } from "./useRunTransactionMutation";
// useWaitForPendingWritesQuery
// useWriteBatchCommitMutation (WriteBatch)
export { useDocumentQuery } from "./useDocumentQuery";
diff --git a/packages/react/src/firestore/useRunTransactionMutation.test.tsx b/packages/react/src/firestore/useRunTransactionMutation.test.tsx
new file mode 100644
index 00000000..a4b70f0c
--- /dev/null
+++ b/packages/react/src/firestore/useRunTransactionMutation.test.tsx
@@ -0,0 +1,138 @@
+import React from "react";
+import { describe, expect, test, beforeEach, vi } from "vitest";
+import { renderHook, act, waitFor } from "@testing-library/react";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { firestore, wipeFirestore } from "~/testing-utils";
+import { useRunTransactionMutation } from "./useRunTransactionMutation";
+import { doc, getDoc, setDoc, type Transaction } from "firebase/firestore";
+
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: { retry: false },
+ mutations: { retry: false },
+ },
+});
+
+const wrapper = ({ children }: { children: React.ReactNode }) => (
+ {children}
+);
+
+describe("useRunTransactionMutation", () => {
+ beforeEach(async () => {
+ queryClient.clear();
+ await wipeFirestore();
+ });
+
+ test("should successfully perform a transaction and update a Firestore document", async () => {
+ const docRef = doc(firestore, "tests", "transactionDoc");
+ await setDoc(docRef, { foo: "bar" });
+
+ const updateFunction = async (transaction: Transaction) => {
+ transaction.set(docRef, { foo: "updatedDoc" });
+ };
+
+ const { result } = renderHook(
+ () => useRunTransactionMutation(firestore, updateFunction),
+ { wrapper }
+ );
+
+ await act(() => result.current.mutate());
+
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
+
+ // Verify the document was actually updated
+ const docSnapshot = await getDoc(docRef);
+ expect(docSnapshot.exists()).toBe(true);
+ expect(docSnapshot.data()).toEqual({ foo: "updatedDoc" });
+ });
+
+ test("should perform a transaction with options and update a Firestore document", async () => {
+ const docRef = doc(firestore, "tests", "transactionDoc");
+
+ await setDoc(docRef, { foo: "bar" });
+
+ const updateFunction = async (transaction: Transaction) => {
+ transaction.set(docRef, { foo: "updatedWithOptions" });
+ };
+
+ const { result } = renderHook(
+ () =>
+ useRunTransactionMutation(firestore, updateFunction, {
+ firestore: { maxAttempts: 1 },
+ }),
+ { wrapper }
+ );
+
+ await act(() => result.current.mutate());
+
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
+
+ const docSnapshot = await getDoc(docRef);
+ expect(docSnapshot.exists()).toBe(true);
+ expect(docSnapshot.data()).toEqual({ foo: "updatedWithOptions" });
+ });
+
+ test("should handle transaction errors correctly", async () => {
+ const updateFunction = async () => {
+ throw new Error("Transaction failed");
+ };
+
+ const { result } = renderHook(
+ () => useRunTransactionMutation(firestore, updateFunction),
+ { wrapper }
+ );
+
+ await act(() => result.current.mutate());
+
+ await waitFor(() => expect(result.current.isError).toBe(true));
+
+ expect(result.current.isError).toBe(true);
+ expect(result.current.error?.message).toBe("Transaction failed");
+ });
+
+ test("should call onSuccess callback when transaction is successful", async () => {
+ const updateFunction = async (transaction: Transaction) => {
+ const docRef = doc(firestore, "tests", "transactionDoc");
+ transaction.set(docRef, { foo: "onSuccessTest" });
+ return "Success";
+ };
+
+ const onSuccessMock = vi.fn();
+
+ const { result } = renderHook(
+ () =>
+ useRunTransactionMutation(firestore, updateFunction, {
+ onSuccess: onSuccessMock,
+ }),
+ { wrapper }
+ );
+
+ await act(() => result.current.mutate());
+
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
+
+ expect(onSuccessMock).toHaveBeenCalled();
+ });
+
+ test("should call onError callback when transaction fails", async () => {
+ const updateFunction = async () => {
+ throw new Error("Transaction failed");
+ };
+
+ const onErrorMock = vi.fn();
+
+ const { result } = renderHook(
+ () =>
+ useRunTransactionMutation(firestore, updateFunction, {
+ onError: onErrorMock,
+ }),
+ { wrapper }
+ );
+
+ await act(() => result.current.mutate());
+
+ await waitFor(() => expect(result.current.isError).toBe(true));
+
+ expect(onErrorMock).toHaveBeenCalled();
+ });
+});
diff --git a/packages/react/src/firestore/useRunTransactionMutation.ts b/packages/react/src/firestore/useRunTransactionMutation.ts
new file mode 100644
index 00000000..365ff86b
--- /dev/null
+++ b/packages/react/src/firestore/useRunTransactionMutation.ts
@@ -0,0 +1,31 @@
+import { useMutation, type UseMutationOptions } from "@tanstack/react-query";
+import {
+ Firestore,
+ FirestoreError,
+ Transaction,
+ runTransaction,
+ TransactionOptions,
+} from "firebase/firestore";
+
+type RunTransactionFunction = (transaction: Transaction) => Promise;
+
+type FirestoreUseMutationOptions = Omit<
+ UseMutationOptions,
+ "mutationFn"
+> & {
+ firestore?: TransactionOptions;
+};
+
+export function useRunTransactionMutation(
+ firestore: Firestore,
+ updateFunction: RunTransactionFunction,
+ options?: FirestoreUseMutationOptions
+) {
+ const { firestore: firestoreOptions, ...queryOptions } = options ?? {};
+
+ return useMutation({
+ ...queryOptions,
+ mutationFn: () =>
+ runTransaction(firestore, updateFunction, firestoreOptions),
+ });
+}