React Hooks library for Firestore, built using the Firebase v9 modular SDK. It utilizes the useSWRSubscription function from SWR library to enable subscription-based data fetching and caching.
Inspired by swr-firestore-v9
# if you use NPM
npm i --save @tatsuokaniwa/swr-firestore
# if you use Yarn
yarn add @tatsuokaniwa/swr-firestore
# if you use pnpm
pnpm i @tatsuokaniwa/swr-firestoreTo use aggregation features (useAggregate, useCollectionGroupAggregate, fetchAggregate, fetchCollectionGroupAggregate):
- Client-side:
firebase >= 9.17.0 - Server-side:
firebase-admin >= 11.5.0(recommended)
import { useCollection, useCollectionCount } from "@tatsuokaniwa/swr-firestore";
import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";
initializeApp();
const db = getFirestore();
type Post = {
content: string;
status: "draft" | "published";
createdAt: Date;
};
export default function App() {
// Conditional Fetching
const [isLogin, setIsLogin] = useState(false);
const { data } = useCollection<Post>(
isLogin && {
path: "posts",
where: [["status", "==", "published"]],
orderBy: [["createdAt", "desc"]],
parseDates: ["createdAt"],
}
);
const { data: postCount } = useCollectionCount<Post>({
path: "posts",
where: [["status", "==", "published"]],
});
return (
<div>
<h1>{postCount} posts</h1>
{data?.map((x, i) => (
<div key={i}>
{x.content} {x.createdAt.toLocaleString()}
</div>
))}
<button onClick={() => setIsLogin(!isLogin)}>Toggle auth</button>
</div>
);
}To perform complex queries like using OR queries or raw QueryConstraint, use the queryConstraints parameter.
However, this method does not provide input completion for field names from type definitions.
import { or, orderBy, where } from "firebase/firestore";
useCollection<City>({
path: "cities",
queryConstraints: [
or(where("capital", "==", true), where("population", ">=", 1000000)),
orderBy("createdAt", "desc"),
],
});You can use the server module to get the SWR key and data.
import { useCollection, useGetDocs } from "@tatsuokaniwa/swr-firestore"
import { getCollection } from "@tatsuokaniwa/swr-firestore/server"
export async function getStaticProps() {
const params = {
path: "posts",
where: [["status", "==", "published"]],
}
const { key, data } = await getCollection<Post>({
...params,
isSubscription: true, // Add the prefix `$sub$` to the SWR key
})
const { key: useGetDocsKey, data: useGetDocsData } = await getCollection<Post>(params)
return {
props: {
fallback: {
[key]: data,
[useGetDocsKey]: useGetDocsData,
}
}
}
}
export default function Page({ fallback }) {
const { data } = useCollection<Post>({
path: "posts",
where: [["status", "==", "published"]],
})
const { data: useGetDocsData } = useGetDocs<Post>({
path: "posts",
where: [["status", "==", "published"]],
})
return (
<SWRConfig value={{ fallback }}>
{data?.map((x, i) => <div key={i}>{x.content}}</div>)}
</SWRConfig>
)
}import {
// SWR Hooks
useCollection, // Subscription for collection
useCollectionCount, // Wrapper for getCountFromServer for collection
useCollectionGroup, // Subscription for collectionGroup
useCollectionGroupCount, // Wrapper for getCountFromServer for collectionGroup
useAggregate, // Aggregation queries (count, sum, average) for collection
useCollectionGroupAggregate, // Aggregation queries for collectionGroup
useDoc, // Subscription for document
useGetDocs, // Fetch documents with firestore's getDocs
useGetDoc, // Fetch document with firestore's getDoc
// Client-side fetchers (without SWR)
fetchDoc, // Fetch single document
fetchCollection, // Fetch collection
fetchCollectionCount, // Count documents in collection
fetchCollectionGroup, // Fetch collection group
fetchCollectionGroupCount, // Count documents in collection group
fetchAggregate, // Aggregation queries for collection
fetchCollectionGroupAggregate, // Aggregation queries for collection group
// Client-side transaction fetcher
fetchDocInTx, // Fetch document within transaction
} from "@tatsuokaniwa/swr-firestore";
import {
getCollection, // Get the SWR key and data for useCollection, useGetDocs
getCollectionCount, // for useCollectionCount
getCollectionGroup, // for useCollectionGroup, useGetDocs
getCollectionGroupCount, // for useCollectionGroupCount
getAggregate, // for useAggregate
getCollectionGroupAggregate, // for useCollectionGroupAggregate
getDoc, // for useDoc, useGetDoc
// Transaction-aware fetchers (for use within db.runTransaction)
getDocInTx,
getCollectionInTx,
getCollectionCountInTx,
getCollectionGroupInTx,
getCollectionGroupCountInTx,
} from "@tatsuokaniwa/swr-firestore/server";import type {
endAt,
endBefore,
limit,
orderBy,
startAfter,
startAt,
where,
} from "firebase/firestore";
type DocumentId = "id";
// First argument of hook, specifies options to firestore, and is also used as a key for SWR.
type KeyParams<T> =
| {
// The path to the collection or document of Firestore.
path: string;
// `Paths` means object's property path, including nested object
where?: [
Paths<T> | DocumentId, // "id" is internally converted to documentId()
Parameters<typeof where>[1],
ValueOf<T> | unknown
][];
orderBy?: [Paths<T> | DocumentId, Parameters<typeof orderBy>[1]][];
startAt?: Parameters<typeof startAt>;
startAfter?: Parameters<typeof startAfter>;
endAt?: Parameters<typeof endAt>;
endBefore?: Parameters<typeof endBefore>;
limit?: number;
limitToLast?: number;
// Array of field names that should be parsed as dates.
parseDates?: Paths<T>[];
}
// OR for more complex query
| {
// The path to the collection or document of Firestore.
path: string;
// raw query constraints from `firebase/firestore`
queryConstraints?:
| [QueryCompositeFilterConstraint, ...Array<QueryNonFilterConstraint>]
| QueryConstraint[];
// Array of field names that should be parsed as dates.
parseDates?: Paths<T>[];
};import type { QueryDocumentSnapshot } from "firebase/firestore";
type DocumentData<T> = T & Pick<QueryDocumentSnapshot, "exists" | "id" | "ref">;Subscription for collection
params: KeyParams | nullswrOptions: Options for SWR hook exceptfetcher
data: data for given path's collectionerror: FirestoreError
import { useCollection } from "@tatsuokaniwa/swr-firestore";
const { data, error } = useCollection<Post>({
path: "posts",
});Wrapper for getCountFromServer for collection
params: KeyParams exceptparseDates| nullswrOptions: Options for SWR hook exceptfetcher
Returns SWRResponse
data: number for given path's collection count resulterror: FirestoreErrorisLoading: if there's an ongoing request and no "loaded data". Fallback data and previous data are not considered "loaded data"isValidating: if there's a request or revalidation loadingmutate(data?, options?): function to mutate the cached data (details)
import { useCollectionCount } from "@tatsuokaniwa/swr-firestore";
const {
data: postCount,
error,
isLoading,
} = useCollectionCount<Post>({
path: "posts",
});Subscription for collectionGroup
params: KeyParams | nullswrOptions: Options for SWR hook exceptfetcher
data: data for given path's collectionGrouperror: FirestoreError
Wrapper for getCountFromServer for collectionGroup
params: KeyParams exceptparseDates| nullswrOptions: Options for SWR hook exceptfetcher
Returns SWRResponse
data: number for given path's collectionGroup count resulterror: FirestoreErrorisLoading: if there's an ongoing request and no "loaded data". Fallback data and previous data are not considered "loaded data"isValidating: if there's a request or revalidation loadingmutate(data?, options?): function to mutate the cached data (details)
Wrapper for getAggregateFromServer for collection. Supports count, sum, and average aggregations in a single query.
params: KeyParams exceptparseDates& { aggregate: AggregateSpec } | nullswrOptions: Options for SWR hook exceptfetcher
Returns SWRResponse
data: aggregation result object with keys matching the aggregate specerror: FirestoreErrorisLoading: if there's an ongoing request and no "loaded data"isValidating: if there's a request or revalidation loadingmutate(data?, options?): function to mutate the cached data
import { useAggregate } from "@tatsuokaniwa/swr-firestore";
type Product = {
name: string;
category: string;
price: number;
stock: number;
};
const { data, error, isLoading } = useAggregate<
Product,
{
totalStock: { type: "sum"; field: "stock" };
averagePrice: { type: "average"; field: "price" };
productCount: { type: "count" };
}
>({
path: "products",
where: [["category", "==", "electronics"]],
aggregate: {
totalStock: { type: "sum", field: "stock" },
averagePrice: { type: "average", field: "price" },
productCount: { type: "count" },
},
});
if (data) {
console.log(data.productCount); // number
console.log(data.totalStock); // number
console.log(data.averagePrice); // number | null (null when no documents)
}Wrapper for getAggregateFromServer for collectionGroup. Supports count, sum, and average aggregations across subcollections.
params: KeyParams exceptparseDates& { aggregate: AggregateSpec } | nullswrOptions: Options for SWR hook exceptfetcher
Returns SWRResponse
data: aggregation result object with keys matching the aggregate specerror: FirestoreErrorisLoading: if there's an ongoing request and no "loaded data"isValidating: if there's a request or revalidation loadingmutate(data?, options?): function to mutate the cached data
import { useCollectionGroupAggregate } from "@tatsuokaniwa/swr-firestore";
type OrderItem = {
productId: string;
price: number;
quantity: number;
};
// Aggregate across all "items" subcollections
const { data } = useCollectionGroupAggregate<
OrderItem,
{
totalRevenue: { type: "sum"; field: "price" };
itemCount: { type: "count" };
}
>({
path: "items",
aggregate: {
totalRevenue: { type: "sum", field: "price" },
itemCount: { type: "count" },
},
});Subscription for document
params: KeyParams exceptwhere,orderBy,limit| nullswrOptions: Options for SWR hook exceptfetcher
data: data for given path's documenterror: FirestoreError
import { useDoc } from "@tatsuokaniwa/swr-firestore";
const { data, error } = useDoc<Post>({
path: `posts/${postId}`,
});Fetch documents with firestore's getDocs function
-
params: KeyParams & { useOfflineCache?: boolean; isCollectionGroup?: boolean } | nullset
isCollectionGroup: trueto get data from collectionGroup -
swrOptions: Options for SWR hook exceptfetcher
Returns SWRResponse
data: data for given path's collectionerror: FirestoreErrorisLoading: if there's an ongoing request and no "loaded data". Fallback data and previous data are not considered "loaded data"isValidating: if there's a request or revalidation loadingmutate(data?, options?): function to mutate the cached data (details)
import { useGetDocs } from "@tatsuokaniwa/swr-firestore";
const { data, error } = useGetDocs<Post>({
path: `posts`,
});
// for collectionGroup
const { data, error } = useGetDocs<Comment>({
path: `comments`,
isCollectionGroup: true,
});Fetch the document with firestore's getDoc function
params: (KeyParams & { useOfflineCache?: boolean }) exceptwhere,orderBy,limit| nullswrOptions: Options for SWR hook exceptfetcher
Returns SWRResponse
data: data for given path's documenterror: FirestoreErrorisLoading: if there's an ongoing request and no "loaded data". Fallback data and previous data are not considered "loaded data"isValidating: if there's a request or revalidation loadingmutate(data?, options?): function to mutate the cached data (details)
import { useGetDoc } from "@tatsuokaniwa/swr-firestore";
const { data, error } = useGetDoc<Post>({
path: `posts/${postId}`,
});These functions fetch data directly from Firestore without SWR caching. Useful for one-off data fetching, imperative data loading, or when you don't need SWR's caching and revalidation features.
Fetch a single document from Firestore
import { fetchDoc } from "@tatsuokaniwa/swr-firestore";
const city = await fetchDoc<City>({
path: "cities/tokyo",
parseDates: ["createdAt"],
});Fetch documents from a collection
import { fetchCollection } from "@tatsuokaniwa/swr-firestore";
const cities = await fetchCollection<City>({
path: "cities",
where: [["population", ">", 1000000]],
orderBy: [["population", "desc"]],
limit: 10,
});Count documents in a collection
import { fetchCollectionCount } from "@tatsuokaniwa/swr-firestore";
const count = await fetchCollectionCount<City>({
path: "cities",
where: [["population", ">", 1000000]],
});Fetch documents from a collection group
import { fetchCollectionGroup } from "@tatsuokaniwa/swr-firestore";
const comments = await fetchCollectionGroup<Comment>({
path: "comments",
where: [["authorId", "==", "user123"]],
limit: 10,
});Count documents in a collection group
import { fetchCollectionGroupCount } from "@tatsuokaniwa/swr-firestore";
const count = await fetchCollectionGroupCount<Comment>({
path: "comments",
where: [["status", "==", "approved"]],
});Fetch aggregation result from a collection
import { fetchAggregate } from "@tatsuokaniwa/swr-firestore";
const result = await fetchAggregate<
Product,
{
count: { type: "count" };
totalStock: { type: "sum"; field: "stock" };
avgPrice: { type: "average"; field: "price" };
}
>({
path: "products",
aggregate: {
count: { type: "count" },
totalStock: { type: "sum", field: "stock" },
avgPrice: { type: "average", field: "price" },
},
});Fetch aggregation result from a collection group
import { fetchCollectionGroupAggregate } from "@tatsuokaniwa/swr-firestore";
const result = await fetchCollectionGroupAggregate<
OrderItem,
{ totalRevenue: { type: "sum"; field: "price" } }
>({
path: "items",
aggregate: {
totalRevenue: { type: "sum", field: "price" },
},
});Fetch a single document within a Firestore transaction (client-side)
Note: Due to Firebase client SDK limitations, only document fetching is supported in transactions. Collection queries within transactions are only available in the server module.
import { getFirestore, runTransaction } from "firebase/firestore";
import { fetchDocInTx } from "@tatsuokaniwa/swr-firestore";
const db = getFirestore();
await runTransaction(db, async (t) => {
const city = await fetchDocInTx<City>(t, {
path: "cities/tokyo",
parseDates: ["createdAt"],
});
if (city) {
t.update(doc(db, "cities/tokyo"), {
population: city.population + 1,
});
}
});Fetch documents using the Firebase Admin SDK and return the SWR key and data
params: KeyParams
Returns Promise<{ key: string; data: DocumentData<T>[]; }>
key: SWR Keydata: documents in the collection for the given path
import { getCollection } from "@tatsuokaniwa/swr-firestore/server";
// For useCollection
const { key, data } = await getCollection<Post>({
path: "posts",
isSubscription: true, // Add the prefix `$sub$` to the SWR key
});
// For useGetDocs
const { key, data } = await getCollection<Post>({ path: "posts" });Fetch document's count using the Firebase Admin SDK and return the SWR key and data
params: KeyParams exceptparseDates
Returns Promise<{ key: string; data: number; }>
key: SWR Keydata: number of documents in the collection for the given path.
import { getCollectionCount } from "@tatsuokaniwa/swr-firestore/server";
// For useCollectionCount
const { key, data } = await getCollectionCount<Post>({ path: "posts" });Fetch documents using the Firebase Admin SDK and return the SWR key and data
params: KeyParams
Returns Promise<{ key: string; data: DocumentData<T>[]; }>
key: SWR Keydata: documents in the collectionGroup for the given path
import { getCollectionGroup } from "@tatsuokaniwa/swr-firestore/server";
// For useCollectionGroup
const { key, data } = await getCollectionGroup<Comment>({
path: "comments",
isSubscription: true, // Add the prefix `$sub$` to the SWR key
});
// For useGetDocs with isCollectionGroup
const { key, data } = await getCollectionGroup<Comment>({ path: "comments" });Fetch document's count using the Firebase Admin SDK and return the SWR key and data
params: KeyParams exceptparseDates
Returns Promise<{ key: string; data: number; }>
key: SWR Keydata: number of documents in the collection group for the given path
import { getCollectionGroupCount } from "@tatsuokaniwa/swr-firestore/server";
// For useCollectionGroupCount
const { key, data } = await getCollectionGroupCount<Comment>({
path: "comments",
});Fetch aggregation result using the Firebase Admin SDK and return the SWR key and data
params: KeyParams exceptparseDates,queryConstraints& { aggregate: AggregateSpec }
Note: queryConstraints is not supported on the server side because the Admin SDK uses a different query builder API.
Returns Promise<{ key: string; data: AggregateResult<TSpec>; }>
key: SWR Keydata: aggregation result object
import { getAggregate } from "@tatsuokaniwa/swr-firestore/server";
// For useAggregate
const { key, data } = await getAggregate<
Product,
{
count: { type: "count" };
totalRevenue: { type: "sum"; field: "price" };
}
>({
path: "products",
aggregate: {
count: { type: "count" },
totalRevenue: { type: "sum", field: "price" },
},
});Fetch aggregation result across subcollections using the Firebase Admin SDK
params: KeyParams exceptparseDates,queryConstraints& { aggregate: AggregateSpec }
Note: queryConstraints is not supported on the server side because the Admin SDK uses a different query builder API.
Returns Promise<{ key: string; data: AggregateResult<TSpec>; }>
key: SWR Keydata: aggregation result object
import { getCollectionGroupAggregate } from "@tatsuokaniwa/swr-firestore/server";
// For useCollectionGroupAggregate
const { key, data } = await getCollectionGroupAggregate<
OrderItem,
{ totalItems: { type: "count" } }
>({
path: "items",
aggregate: {
totalItems: { type: "count" },
},
});Fetch the document using the Firebase Admin SDK and return the SWR key and data
params: KeyParams
Returns Promise<{ key: string; data: DocumentData<T>; }>
key: SWR Keydata: data for given path's document
import { getDoc } from "@tatsuokaniwa/swr-firestore/server";
// For useDoc
const { key, data } = await getDoc<Post>({
path: `posts/${postId}`,
isSubscription: true, // Add the prefix `$sub$` to the SWR key
});
// For useGetDoc
const { key, data } = await getDoc<Post>({ path: `posts/${postId}` });Type-safe document fetcher for use within Firestore transactions
transaction: Firebase Admin SDK Transaction objectparams: KeyParams exceptwhere,orderBy,limit
Returns Promise<DocumentData<T> | undefined>
- Returns the document data, or undefined if the document does not exist
import { getFirestore } from "firebase-admin/firestore";
import { getDocInTx } from "@tatsuokaniwa/swr-firestore/server";
const db = getFirestore();
await db.runTransaction(async (t) => {
const city = await getDocInTx<City>(t, {
path: "cities/tokyo",
parseDates: ["createdAt"],
});
if (city) {
t.update(db.doc("cities/tokyo"), {
population: city.population + 1,
});
}
});Type-safe collection fetcher for use within Firestore transactions
transaction: Firebase Admin SDK Transaction objectparams: KeyParams
Returns Promise<DocumentData<T>[]>
- Returns an array of document data
import { getFirestore } from "firebase-admin/firestore";
import { getCollectionInTx } from "@tatsuokaniwa/swr-firestore/server";
const db = getFirestore();
await db.runTransaction(async (t) => {
const cities = await getCollectionInTx<City>(t, {
path: "cities",
where: [["population", ">", 1000000]],
orderBy: [["population", "desc"]],
limit: 10,
});
cities.forEach((city) => {
t.update(db.doc(`cities/${city.id}`), {
isLargeCity: true,
});
});
});Type-safe collection count fetcher for use within Firestore transactions
transaction: Firebase Admin SDK Transaction objectparams: KeyParams exceptparseDates
Returns Promise<number>
await db.runTransaction(async (t) => {
const count = await getCollectionCountInTx<City>(t, {
path: "cities",
where: [["population", ">", 1000000]],
});
console.log(`Found ${count} large cities`);
});Type-safe collection group fetcher for use within Firestore transactions
transaction: Firebase Admin SDK Transaction objectparams: KeyParams
Returns Promise<DocumentData<T>[]>
await db.runTransaction(async (t) => {
const comments = await getCollectionGroupInTx<Comment>(t, {
path: "comments",
where: [["authorId", "==", "user123"]],
limit: 10,
});
// comments is DocumentData<Comment>[]
});Type-safe collection group count fetcher for use within Firestore transactions
transaction: Firebase Admin SDK Transaction objectparams: KeyParams exceptparseDates
Returns Promise<number>
await db.runTransaction(async (t) => {
const count = await getCollectionGroupCountInTx<Comment>(t, {
path: "comments",
where: [["status", "==", "approved"]],
});
console.log(`Found ${count} approved comments`);
});Before running the test, you need to install the Firebase tools.
npm run test:ciMIT