Skip to content

Commit 513e8a5

Browse files
authored
feat(react/firestore): add useSetDocumentMutation (#158)
* feat(react/firestore): add useSetDocumentMutation * refactor: move variables to mutation args
1 parent 2f0ca8b commit 513e8a5

File tree

3 files changed

+196
-0
lines changed

3 files changed

+196
-0
lines changed

packages/react/src/firestore/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@ export { useDocumentQuery } from "./useDocumentQuery";
88
export { useCollectionQuery } from "./useCollectionQuery";
99
export { useGetAggregateFromServerQuery } from "./useGetAggregateFromServerQuery";
1010
export { useGetCountFromServerQuery } from "./useGetCountFromServerQuery";
11+
export { useSetDocumentMutation } from "./useSetDocumentMutation";
1112
export { useNamedQuery } from "./useNamedQuery";
1213
export { useDeleteDocumentMutation } from "./useDeleteDocumentMutation";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import { renderHook, waitFor, act } from "@testing-library/react";
2+
import { doc, type DocumentReference, getDoc } from "firebase/firestore";
3+
import { beforeEach, describe, expect, test } from "vitest";
4+
import { useSetDocumentMutation } from "./useSetDocumentMutation";
5+
6+
import {
7+
expectFirestoreError,
8+
firestore,
9+
wipeFirestore,
10+
} from "~/testing-utils";
11+
import { queryClient, wrapper } from "../../utils";
12+
13+
describe("useSetDocumentMutation", () => {
14+
beforeEach(async () => {
15+
await wipeFirestore();
16+
queryClient.clear();
17+
});
18+
19+
test("successfully sets a new document", async () => {
20+
const docRef = doc(firestore, "tests", "setTest");
21+
const testData = { foo: "bar", num: 42 };
22+
23+
const { result } = renderHook(() => useSetDocumentMutation(docRef), {
24+
wrapper,
25+
});
26+
27+
expect(result.current.isPending).toBe(false);
28+
expect(result.current.isIdle).toBe(true);
29+
30+
await act(() => result.current.mutate(testData));
31+
32+
await waitFor(() => {
33+
expect(result.current.isSuccess).toBe(true);
34+
});
35+
36+
const snapshot = await getDoc(docRef);
37+
expect(snapshot.exists()).toBe(true);
38+
expect(snapshot.data()).toEqual(testData);
39+
});
40+
41+
test("successfully overwrites existing document", async () => {
42+
const docRef = doc(firestore, "tests", "overwriteTest");
43+
const initialData = { foo: "initial", num: 1 };
44+
const newData = { foo: "updated", num: 2 };
45+
46+
const { result } = renderHook(() => useSetDocumentMutation(docRef), {
47+
wrapper,
48+
});
49+
50+
await act(() => result.current.mutate(initialData));
51+
52+
await waitFor(() => {
53+
expect(result.current.isSuccess).toBe(true);
54+
});
55+
56+
let snapshot = await getDoc(docRef);
57+
expect(snapshot.data()).toEqual(initialData);
58+
59+
const { result: result2 } = renderHook(
60+
() => useSetDocumentMutation(docRef),
61+
{ wrapper }
62+
);
63+
64+
await act(() => result2.current.mutate(newData));
65+
66+
await waitFor(() => {
67+
expect(result2.current.isSuccess).toBe(true);
68+
});
69+
70+
snapshot = await getDoc(docRef);
71+
expect(snapshot.data()).toEqual(newData);
72+
});
73+
74+
test("handles type-safe document data", async () => {
75+
interface TestDoc {
76+
foo: string;
77+
num: number;
78+
}
79+
80+
const docRef = doc(
81+
firestore,
82+
"tests",
83+
"typedDoc"
84+
) as DocumentReference<TestDoc>;
85+
const testData: TestDoc = { foo: "test", num: 123 };
86+
87+
const { result } = renderHook(() => useSetDocumentMutation(docRef), {
88+
wrapper,
89+
});
90+
91+
await act(() => result.current.mutate(testData));
92+
93+
await waitFor(() => {
94+
expect(result.current.isSuccess).toBe(true);
95+
});
96+
97+
const snapshot = await getDoc(docRef);
98+
const data = snapshot.data();
99+
expect(data?.foo).toBe("test");
100+
expect(data?.num).toBe(123);
101+
});
102+
103+
test("handles errors when setting to restricted collection", async () => {
104+
const restrictedDocRef = doc(firestore, "restrictedCollection", "someDoc");
105+
const testData = { foo: "bar" };
106+
107+
const { result } = renderHook(
108+
() => useSetDocumentMutation(restrictedDocRef),
109+
{ wrapper }
110+
);
111+
112+
await act(() => result.current.mutate(testData));
113+
114+
await waitFor(() => {
115+
expect(result.current.isError).toBe(true);
116+
});
117+
118+
expectFirestoreError(result.current.error, "permission-denied");
119+
});
120+
121+
test("calls onSuccess callback after setting document", async () => {
122+
const docRef = doc(firestore, "tests", "callbackTest");
123+
const testData = { foo: "callback" };
124+
let callbackCalled = false;
125+
126+
const { result } = renderHook(
127+
() =>
128+
useSetDocumentMutation(docRef, {
129+
onSuccess: () => {
130+
callbackCalled = true;
131+
},
132+
}),
133+
{ wrapper }
134+
);
135+
136+
await act(() => result.current.mutate(testData));
137+
138+
await waitFor(() => {
139+
expect(result.current.isSuccess).toBe(true);
140+
});
141+
142+
expect(callbackCalled).toBe(true);
143+
const snapshot = await getDoc(docRef);
144+
expect(snapshot.data()?.foo).toBe("callback");
145+
});
146+
147+
test("handles empty data object", async () => {
148+
const docRef = doc(firestore, "tests", "emptyDoc");
149+
const emptyData = {};
150+
151+
const { result } = renderHook(() => useSetDocumentMutation(docRef), {
152+
wrapper,
153+
});
154+
155+
await act(() => result.current.mutate(emptyData));
156+
157+
await waitFor(() => {
158+
expect(result.current.isSuccess).toBe(true);
159+
});
160+
161+
const snapshot = await getDoc(docRef);
162+
expect(snapshot.exists()).toBe(true);
163+
expect(snapshot.data()).toEqual({});
164+
});
165+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { useMutation, type UseMutationOptions } from "@tanstack/react-query";
2+
import {
3+
type DocumentReference,
4+
type FirestoreError,
5+
type WithFieldValue,
6+
type DocumentData,
7+
setDoc,
8+
} from "firebase/firestore";
9+
10+
type FirestoreUseMutationOptions<
11+
TData = unknown,
12+
TError = Error,
13+
AppModelType extends DocumentData = DocumentData
14+
> = Omit<
15+
UseMutationOptions<TData, TError, WithFieldValue<AppModelType>>,
16+
"mutationFn"
17+
>;
18+
19+
export function useSetDocumentMutation<
20+
AppModelType extends DocumentData = DocumentData,
21+
DbModelType extends DocumentData = DocumentData
22+
>(
23+
documentRef: DocumentReference<AppModelType, DbModelType>,
24+
options?: FirestoreUseMutationOptions<void, FirestoreError, AppModelType>
25+
) {
26+
return useMutation<void, FirestoreError, WithFieldValue<AppModelType>>({
27+
...options,
28+
mutationFn: (data) => setDoc(documentRef, data),
29+
});
30+
}

0 commit comments

Comments
 (0)