diff --git a/packages/firebase/useFirestore/index.md b/packages/firebase/useFirestore/index.md index 0c0bac8742e..8dd6edb4cbe 100644 --- a/packages/firebase/useFirestore/index.md +++ b/packages/firebase/useFirestore/index.md @@ -8,7 +8,7 @@ Reactive [Firestore](https://firebase.google.com/docs/firestore) binding. Making ## Usage -```js {9,12,17} +```js {9,12,17,22} import { computed, ref } from 'vue' import { initializeApp } from 'firebase/app' import { collection, doc, getFirestore, limit, orderBy, query } from 'firebase/firestore' @@ -23,9 +23,15 @@ const todos = useFirestore(collection(db, 'todos')) const user = useFirestore(doc(db, 'users', 'my-user-id')) // you can also use ref value for reactive query -const postLimit = ref(10) -const postsQuery = computed(() => query(collection(db, 'posts'), orderBy('createdAt', 'desc'), limit(postLimit.value))) +const postsLimit = ref(10) +const postsQuery = computed(() => query(collection(db, 'posts'), orderBy('createdAt', 'desc'), limit(postsLimit.value))) const posts = useFirestore(postsQuery) + +// you can use the boolean value to tell a query when it is ready to run +// when it gets falsy value, return the initial value +const userId = ref('') +const userQuery = computed(() => !!userId.value && doc(db, 'users', userId.value)) +const userData = useFirestore(userQuery, null) ``` ## Share across instances diff --git a/packages/firebase/useFirestore/index.test.ts b/packages/firebase/useFirestore/index.test.ts index 5534ea5c944..7a3e47f8884 100644 --- a/packages/firebase/useFirestore/index.test.ts +++ b/packages/firebase/useFirestore/index.test.ts @@ -1,27 +1,101 @@ -import { ref } from 'vue-demi' -import { onSnapshot } from 'firebase/firestore' -import type { Mock } from 'vitest' +import { collection, doc } from 'firebase/firestore' +import type { Firestore } from 'firebase/firestore' +import { computed, nextTick, ref } from 'vue-demi' import { useFirestore } from './index' -vi.mock('firebase/firestore', () => ({ - onSnapshot: vi.fn(), -})) +const dummyFirestore = {} as Firestore + +const getMockSnapFromRef = (docRef: any) => ({ + id: `${docRef.path}-id`, + data: () => (docRef), +}) + +const getData = (docRef: any) => { + const data = docRef.data() + Object.defineProperty(data, 'id', { + value: docRef.id.toString(), + writable: false, + }) + return data +} + +const unsubscribe = vi.fn() + +vi.mock('firebase/firestore', () => { + const doc = vi.fn((_: Firestore, path: string) => { + if (path.includes('//')) + throw new Error('Invalid segment') + return { path } + }) + + const collection = vi.fn((_: Firestore, path: string) => { + if (path.includes('//')) + throw new Error('Invalid segment') + return { path } + }) + + const onSnapshot = vi.fn((docRef: any, callbackFn: (payload: any) => {}) => { + callbackFn({ + ...getMockSnapFromRef(docRef), + docs: [getMockSnapFromRef(docRef)], + }) + return unsubscribe + }) + return { onSnapshot, collection, doc } +}) describe('useFirestore', () => { beforeEach(() => { - (onSnapshot as Mock).mockClear() + unsubscribe.mockClear() + }) + + it('should get `users` collection data', () => { + const collectionRef = collection(dummyFirestore, 'users') + const data = useFirestore(collectionRef) + expect(data.value).toEqual([getData(getMockSnapFromRef(collectionRef))]) + }) + + it('should get `users/userId` document data', () => { + const docRef = doc(dummyFirestore, 'users/userId') + const data = useFirestore(docRef) + expect(data.value).toEqual(getData(getMockSnapFromRef(docRef))) + }) + + it('should get `posts` computed query data', () => { + const queryRef = collection(dummyFirestore, 'posts') + const data = useFirestore(computed(() => queryRef)) + expect(data.value).toEqual([getData(getMockSnapFromRef(queryRef))]) + }) + + it('should get initial value when pass falsy value', () => { + const collectionRef = collection(dummyFirestore, 'todos') + const falsy = computed(() => false as boolean && collectionRef) + const data = useFirestore(falsy, [{ id: 'default' }]) + expect(data.value).toEqual([{ id: 'default' }]) }) - it('should call onSnapshot with document reference', () => { - const docRef = { path: 'users' } as any - useFirestore(docRef) - expect((onSnapshot as Mock).mock.calls[0][0]).toStrictEqual(docRef) + it('should get reactive query data & unsubscribe previous query when re-querying', async () => { + const queryRef = collection(dummyFirestore, 'posts') + const reactiveQueryRef = ref(queryRef) + const data = useFirestore(reactiveQueryRef) + expect(data.value).toEqual([getData(getMockSnapFromRef(reactiveQueryRef.value))]) + reactiveQueryRef.value = collection(dummyFirestore, 'todos') + await nextTick() + expect(unsubscribe).toHaveBeenCalled() + expect(data.value).toEqual([getData(getMockSnapFromRef(reactiveQueryRef.value))]) }) - it('should call onSnapshot with ref value of document reference', () => { - const docRef = { path: 'posts' } as any - const refOfDocRef = ref(docRef) - useFirestore(refOfDocRef) - expect((onSnapshot as Mock).mock.calls[0][0]).toStrictEqual(docRef) + it('should get user data only when user id exists', async () => { + const userId = ref('') + const queryRef = computed(() => !!userId.value && collection(dummyFirestore, `users/${userId.value}/posts`)) + const data = useFirestore(queryRef, [{ id: 'default' }]) + expect(data.value).toEqual([{ id: 'default' }]) + userId.value = 'userId' + await nextTick() + expect(data.value).toEqual([getData(getMockSnapFromRef(collection(dummyFirestore, `users/${userId.value}/posts`)))]) + userId.value = '' + await nextTick() + expect(unsubscribe).toHaveBeenCalled() + expect(data.value).toEqual([{ id: 'default' }]) }) }) diff --git a/packages/firebase/useFirestore/index.ts b/packages/firebase/useFirestore/index.ts index 49251adfe10..a37352f4d77 100644 --- a/packages/firebase/useFirestore/index.ts +++ b/packages/firebase/useFirestore/index.ts @@ -34,24 +34,24 @@ function isDocumentReference(docRef: any): docRef is DocumentReference { } export function useFirestore( - maybeDocRef: MaybeRef>, + maybeDocRef: MaybeRef | false>, initialValue: T, options?: UseFirestoreOptions ): Ref export function useFirestore( - maybeDocRef: MaybeRef>, + maybeDocRef: MaybeRef | false>, initialValue: T[], options?: UseFirestoreOptions ): Ref // nullable initial values export function useFirestore( - maybeDocRef: MaybeRef>, + maybeDocRef: MaybeRef | false>, initialValue?: T | undefined, options?: UseFirestoreOptions, ): Ref export function useFirestore( - maybeDocRef: MaybeRef>, + maybeDocRef: MaybeRef | false>, initialValue?: T[], options?: UseFirestoreOptions ): Ref @@ -63,7 +63,7 @@ export function useFirestore( * @see https://vueuse.org/useFirestore */ export function useFirestore( - maybeDocRef: MaybeRef>, + maybeDocRef: MaybeRef | false>, initialValue: any = undefined, options: UseFirestoreOptions = {}, ) { @@ -72,41 +72,35 @@ export function useFirestore( autoDispose = true, } = options - const refOfDocRef = isRef(maybeDocRef) ? maybeDocRef : computed(() => maybeDocRef) + const refOfDocRef = isRef(maybeDocRef) + ? maybeDocRef + : computed(() => maybeDocRef) - if (isDocumentReference(refOfDocRef.value)) { - const data = ref(initialValue) as Ref - let close = () => { } + let close = () => { } + const data = ref(initialValue) as Ref - watch(refOfDocRef, (docRef) => { - close() + watch(refOfDocRef, (docRef) => { + close() + if (!refOfDocRef.value) { + data.value = initialValue + } + else if (isDocumentReference(refOfDocRef.value)) { close = onSnapshot(docRef as DocumentReference, (snapshot) => { data.value = getData(snapshot) || null }, errorHandler) - }, { immediate: true }) + } + else { + close = onSnapshot(docRef as Query, (snapshot) => { + data.value = snapshot.docs.map(getData).filter(isDef) + }, errorHandler) + } + }, { immediate: true }) + if (autoDispose && !isDocumentReference(refOfDocRef.value)) { tryOnScopeDispose(() => { close() }) - - return data } - else { - const data = ref(initialValue) as Ref - let close = () => { } - watch(refOfDocRef, (docRef) => { - close() - close = onSnapshot(docRef as Query, (snapshot) => { - data.value = snapshot.docs.map(getData).filter(isDef) - }, errorHandler) - }, { immediate: true }) - - if (autoDispose) { - tryOnScopeDispose(() => { - close() - }) - } - return data - } + return data }