Skip to content
This repository has been archived by the owner on Jan 20, 2025. It is now read-only.
/ firestore-hooks Public archive

Simple typed React hooks and functions for handling Firestore documents

License

Notifications You must be signed in to change notification settings

0x80/firestore-hooks

Repository files navigation

Firestore Hooks

This library is now deprecated. Please use @typed-firestore/react instead.

Installation

npm add firestore-hooks

Features

  • Convenient use of Typescript generics
  • Uses the same FsDocument abstraction as used in firestore-server-utils, to unify server and client code.
  • Throw errors instead of returning them. See errors.

Usage

Hooks

Hook Description
useDocument Use a document and subscribe to changes
useDocumentData Use only the data part of a document and subscribe to changes
useDocumentMaybe Use a document that might not exist
useDocumentOnce Use a document once and do not subscribe for changes
useCollection Query a collection and subscribe for changes

Regular Functions

These might be useful for when you use something like ReactQuery and you want to fetch a document outside of a component/hook.

Function Description
getDocument Fetch a document
getDocumentData Fetch only the data part of a document
getDocumentMaybe Fetch a document that might not exist
getDocumentFromTransaction Fetch a document as part of a transaction
getDocumentFromTransactionMaybe Fetch a document that might not exist as part of a transaction

Example

The API is optimized for globally configured collection refs. I think it is good practice to reuse these refs when referring to collections, because it reduces boilerplate and prevents typos.

For example, create a global file like db-refs.ts, and store the refs by collection name. For subcollections I like to use a function that returns the ref based on the document id.

import { collection, getFirestore } from "firebase/firestore";

const db = getFirestore();

export const refs = {
  users: collection(db, "users"),
  userEvents: (userId: string) => collection(db, `users/${userId}/events`),
};

Then in a component you would do something like this:

import { useDocument } from "firestore-hooks";
import { UpdateData } from "firebase/firestore";
//...

const [user, isLoading] = useDocument<User>(refs.users, userId);

if (isLoading) {
  return <LoadingIndicator/>;
}

// The data field will be typed to User here
console.log(`User ${user.id} has display name: ${user.data.displayName}`);

// Use the ref handle to mutate the document
user.ref.update({ displayName: "new name" } satisfies UpdateData<User>);

Here we use satisfies to ensure that the update data is compatible with the document data type. The UpdateData type you can import from the firebase/firestore package and it is similar to Partial but also understands dot-notation for updating nested fields.

Personally, I prefer to do all document mutations server-side via an API endpoint. You might want to consider that, especially if you have complete control over the development of client apps or older versions could be around for a while like with react-native. It can also save you from having to write complex database rules for your Firestore documents.

FsDocument

The original Firestore API, while quite convenient, seems more verbose than needed for the vast majority of use-cases. Data is retrieved from the snapshot with a data() but it accepts optional parameters that you will likely never use. Also, Typescript generics are not available and the withConverter API, that could be used to type your data, is overkill if you are not interested in converting your data.

A lot of libraries that make abstractions for Firestore (e.g. react-firebase-hooks) merge the id and data into a single object. I think this is not very elegant, because in Firestore the document id is clearly separated from the data. Bringing the two together not only makes it harder to type your data, but also potentially creates a naming collision, because a document is allowed to have an id field as part of its data, separate from the actual document id.

The most convenient document abstraction I can come up with is:

export type FsDocument<T> = {
  id: string;
  data: T;
  ref: DocumentReference;
};

The ref is included, because sometimes after working with the data, you then want to mutate the same document with set or update.

It is not called Document, because in web environments that is already the global HTML document type, and so IDEs tend to pick the wrong type when automatically generating imports for you.

Errors

Exceptions for Firestore documents and collection queries are rare in production and therefore it was chosen for this library to not to handle them in every calling context.

The most common errors are "an index is required for this query" and attempting to fetch a document that doesn't exist. Both of which are typically caught in development and testing and should not occur in production code.

In some cases you actually know upfront that the document might not exist, so for those instances there are the *Maybe variants like useDocumentMaybe(). These functions do not throw but simply return undefined if the document does not exist. In this case it is not considered an error.

About

Simple typed React hooks and functions for handling Firestore documents

Resources

License

Stars

Watchers

Forks

Packages

No packages published