javascriptfirebasegoogle-cloud-firestore

How to pass a Firestore reference to a locally linked module


I have a utility function that works fine as long as I keep it in my codebase. It takes a CollectionReference and a document id as arguments.

When I move it to a shared library I get the following error:

FirebaseError: Expected first argument to collection() to be a CollectionReference, a DocumentReference or FirebaseFirestore

The collection reference fails the instanceof check that is done internally by firebase/firestore.

The code exported from my library is clean ESM and I have made sure that all places import from firebase/firestore and use the same versions. So I don't know what else I can try.

Here is the code as it is exported by the library:

import { CollectionReference, doc, getDoc } from "firebase/firestore";
import { useEffect, useState } from "react";
function useDocumentDataOnce(collectionRef, documentId) {
  const [data, setData] = useState();
  useEffect(() => {
    const fetchData = async () => {
      if (!documentId) {
        return;
      }
      console.log(
        "+++ collectionRef instanceof CollectionReference?",
        collectionRef instanceof CollectionReference
      );
      const ref = doc(collectionRef, documentId);
      const snapshot = await getDoc(ref);
      if (snapshot.exists()) {
        setData(snapshot.data());
      } else {
        throw new Error(`No document at ${collectionRef.path}/${documentId}`);
      }
    };
    fetchData().catch(console.error);
  }, [collectionRef, documentId]);
  return data;
}

If I use the exact same code, but imported from my codebase it works fine:

import { CollectionReference, doc, getDoc } from "firebase/firestore";
import { useEffect, useState } from "react";

export function useDocumentDataOnce<T>(
  collectionRef: CollectionReference,
  documentId?: string
) {
  const [data, setData] = useState<T>();

  useEffect(() => {
    const fetchData = async () => {
      if (!documentId) {
        return;
      }

      console.log(
        "+++ collectionRef instanceof CollectionReference?",
        collectionRef instanceof CollectionReference
      );

      const ref = doc(collectionRef, documentId);

      const snapshot = await getDoc(ref);
      if (snapshot.exists()) {
        setData(snapshot.data() as T);
      } else {
        throw new Error(`No document at ${collectionRef.path}/${documentId}`);
      }
    };

    fetchData().catch(console.error);
  }, [collectionRef, documentId]); // Add ref to the dependency array

  return data;
}

Here's how the code is used on a Next.js page:

"use client";

import { db, useUserId } from "@/lib/firebase";
import { useDocumentDataOnce } from "failurebase";
import { collection } from "firebase/firestore";

type User = {
  displayName: string;
};

export default function UserProfilePage() {
  const userId = useUserId();
  const usersCollectionRef = collection(db, "users");

  const user = useDocumentDataOnce<User>(usersCollectionRef, userId);

  return <div>Hello {user?.displayName ?? "unknown"}</div>;
}

The "use client" directive makes this page run only client-side in the browser.

The error comes from this line in useDocumentDataOnce:

const ref = doc(collectionRef, documentId);

In the firebase-js-sdk library it comes down to this check on line 578 of reference.ts:

if (
      !(parent instanceof DocumentReference) &&
      !(parent instanceof CollectionReference)
    ) {
      throw new FirestoreError(
        Code.INVALID_ARGUMENT,
        'Expected first argument to collection() to be a CollectionReference, ' +
          'a DocumentReference or FirebaseFirestore'
      );
    }

You can find the library code here. As you can see I have also made firebase a peer dependency, so nothing is bundled with my library code.

The library is also published on NPM. You can install it with failurebase@next

This is driving me nuts. Any idea what might be causing this?


Solution

  • So I found that if I publish and install my module via NPM, it works.

    The problem is not specific to Firebase, but the way (P)NPM linking works when developing modules locally. The instanceof checks, that Firebase uses internally to validate a reference, will fail if you use normal linking.

    I was passing a CollectionReference instance across the module boundary to doc() and the check in that function fails because both modules reference a different class prototype.

    I have tried (p)npm link as well as (p)npm add ../path/to/local/module which creates a file: link in package.json. With both approaches instanceof returns false.

    A solution is to use yalc, which fortunately is easy.

    Another solution would be if the Firebase team would replace the instanceof check with a basic check on object properties. I think this is preferable, as it allows people to more easily develop libraries based on Firebase.

    The check seems to be there mainly to warn the user for invalid use of the methods, so I think doing a check based on object properties should be sufficient.