@tatsuokaniwa/swr-firestore
v2.1.0
Published
React Hooks library for Firestore using SWR's subscription feature.
Readme
swr-firestore
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
Installation
# 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-firestoreRequirements for Aggregation Queries
To use aggregation features (useAggregate, useCollectionGroupAggregate, fetchAggregate, fetchCollectionGroupAggregate):
- Client-side:
firebase >= 9.17.0 - Server-side:
firebase-admin >= 11.5.0(recommended)
Usage
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>
);
}For more complex queries
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"),
],
});SSG and SSR
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>
)
}API
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";Type definitions for parameters
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>[];
};Type definitions for return data
import type { QueryDocumentSnapshot } from "firebase/firestore";
type DocumentData<T> = T & Pick<QueryDocumentSnapshot, "exists" | "id" | "ref">;useCollection(params, swrOptions)
Subscription for collection
Parameters
params: KeyParams | nullswrOptions: Options for SWR hook exceptfetcher
Return values
data: data for given path's collectionerror: FirestoreError
import { useCollection } from "@tatsuokaniwa/swr-firestore";
const { data, error } = useCollection<Post>({
path: "posts",
});useCollectionCount(params, swrOptions)
Wrapper for getCountFromServer for collection
Parameters
params: KeyParams exceptparseDates| nullswrOptions: Options for SWR hook exceptfetcher
Return values
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",
});useCollectionGroup(params, swrOptions)
Subscription for collectionGroup
Parameters
params: KeyParams | nullswrOptions: Options for SWR hook exceptfetcher
Return values
data: data for given path's collectionGrouperror: FirestoreError
useCollectionGroupCount(params, swrOptions)
Wrapper for getCountFromServer for collectionGroup
Parameters
params: KeyParams exceptparseDates| nullswrOptions: Options for SWR hook exceptfetcher
Return values
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)
useAggregate(params, swrOptions)
Wrapper for getAggregateFromServer for collection. Supports count, sum, and average aggregations in a single query.
Parameters
params: KeyParams exceptparseDates& { aggregate: AggregateSpec } | nullswrOptions: Options for SWR hook exceptfetcher
Return values
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)
}useCollectionGroupAggregate(params, swrOptions)
Wrapper for getAggregateFromServer for collectionGroup. Supports count, sum, and average aggregations across subcollections.
Parameters
params: KeyParams exceptparseDates& { aggregate: AggregateSpec } | nullswrOptions: Options for SWR hook exceptfetcher
Return values
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" },
},
});useDoc(params, swrOptions)
Subscription for document
Parameters
params: KeyParams exceptwhere,orderBy,limit| nullswrOptions: Options for SWR hook exceptfetcher
Return values
data: data for given path's documenterror: FirestoreError
import { useDoc } from "@tatsuokaniwa/swr-firestore";
const { data, error } = useDoc<Post>({
path: `posts/${postId}`,
});useGetDocs(params, swrOptions)
Fetch documents with firestore's getDocs function
Parameters
params: KeyParams & { useOfflineCache?: boolean; isCollectionGroup?: boolean } | nullset
isCollectionGroup: trueto get data from collectionGroupswrOptions: Options for SWR hook exceptfetcher
Return values
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,
});useGetDoc(params, swrOptions)
Fetch the document with firestore's getDoc function
Parameters
params: (KeyParams & { useOfflineCache?: boolean }) exceptwhere,orderBy,limit| nullswrOptions: Options for SWR hook exceptfetcher
Return values
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}`,
});Client-side fetchers
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.
fetchDoc(params)
Fetch a single document from Firestore
import { fetchDoc } from "@tatsuokaniwa/swr-firestore";
const city = await fetchDoc<City>({
path: "cities/tokyo",
parseDates: ["createdAt"],
});fetchCollection(params)
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,
});fetchCollectionCount(params)
Count documents in a collection
import { fetchCollectionCount } from "@tatsuokaniwa/swr-firestore";
const count = await fetchCollectionCount<City>({
path: "cities",
where: [["population", ">", 1000000]],
});fetchCollectionGroup(params)
Fetch documents from a collection group
import { fetchCollectionGroup } from "@tatsuokaniwa/swr-firestore";
const comments = await fetchCollectionGroup<Comment>({
path: "comments",
where: [["authorId", "==", "user123"]],
limit: 10,
});fetchCollectionGroupCount(params)
Count documents in a collection group
import { fetchCollectionGroupCount } from "@tatsuokaniwa/swr-firestore";
const count = await fetchCollectionGroupCount<Comment>({
path: "comments",
where: [["status", "==", "approved"]],
});fetchAggregate(params)
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" },
},
});fetchCollectionGroupAggregate(params)
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" },
},
});fetchDocInTx(transaction, params)
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,
});
}
});Server module
getCollection(params)
Fetch documents using the Firebase Admin SDK and return the SWR key and data
Parameters
params: KeyParams
Return values
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" });getCollectionCount(params)
Fetch document's count using the Firebase Admin SDK and return the SWR key and data
Parameters
params: KeyParams exceptparseDates
Return values
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" });getCollectionGroup(params)
Fetch documents using the Firebase Admin SDK and return the SWR key and data
Parameters
params: KeyParams
Return values
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" });getCollectionGroupCount(params)
Fetch document's count using the Firebase Admin SDK and return the SWR key and data
Parameters
params: KeyParams exceptparseDates
Return values
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",
});getAggregate(params)
Fetch aggregation result using the Firebase Admin SDK and return the SWR key and data
Parameters
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.
Return values
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" },
},
});getCollectionGroupAggregate(params)
Fetch aggregation result across subcollections using the Firebase Admin SDK
Parameters
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.
Return values
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" },
},
});getDoc(params)
Fetch the document using the Firebase Admin SDK and return the SWR key and data
Parameters
params: KeyParams
Return values
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}` });getDocInTx(transaction, params)
Type-safe document fetcher for use within Firestore transactions
Parameters
transaction: Firebase Admin SDK Transaction objectparams: KeyParams exceptwhere,orderBy,limit
Return values
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,
});
}
});getCollectionInTx(transaction, params)
Type-safe collection fetcher for use within Firestore transactions
Parameters
transaction: Firebase Admin SDK Transaction objectparams: KeyParams
Return values
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,
});
});
});getCollectionCountInTx(transaction, params)
Type-safe collection count fetcher for use within Firestore transactions
Parameters
transaction: Firebase Admin SDK Transaction objectparams: KeyParams exceptparseDates
Return values
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`);
});getCollectionGroupInTx(transaction, params)
Type-safe collection group fetcher for use within Firestore transactions
Parameters
transaction: Firebase Admin SDK Transaction objectparams: KeyParams
Return values
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>[]
});getCollectionGroupCountInTx(transaction, params)
Type-safe collection group count fetcher for use within Firestore transactions
Parameters
transaction: Firebase Admin SDK Transaction objectparams: KeyParams exceptparseDates
Return values
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`);
});Custom Firestore Instance
All functions and hooks accept an optional db parameter to use a specific Firestore instance instead of the default getFirestore().
// Client-side
const data = await fetchDoc<City>({
path: "cities/tokyo",
db: getFirestore(secondaryApp),
});
// Server-side
const { data } = await getDoc<City>({
path: "cities/tokyo",
db: getFirestore(adminApp),
});
// React hooks
const { data } = useDoc<City>({
path: "cities/tokyo",
db: getFirestore(secondaryApp),
});When db is omitted, it falls back to getFirestore() as before, so existing code is not affected.
Testing
Before running the test, you need to install the Firebase tools.
npm run test:ciLicense
MIT
