@manufac/firebase
v1.0.4
Published
A typed Firebase utility library for React applications. Provides type-safe Firestore CRUD operations, real-time subscriptions, Firebase Storage helpers, and authentication — all wired through a React context provider.
Readme
@manufac/firebase
A typed Firebase utility library for React applications. Provides type-safe Firestore CRUD operations, real-time subscriptions, Firebase Storage helpers, and authentication — all wired through a React context provider.
Setup
1. Define your collection schema
Use Zod to define schemas for each collection. Extend BaseDataSchema to inherit the required id, createdAt, and updatedAt fields.
// schema.ts
import { z } from "zod";
import { BaseDataSchema } from "@manufac/firebase";
export const ProductSchema = BaseDataSchema.extend({
name: z.string(),
price: z.number(),
isAvailable: z.boolean(),
});
export type Product = z.infer<typeof ProductSchema>;2. Define your collection map
Create a collection map that links collection name strings to their Zod schemas. This is the type-safe contract used by all hooks and utilities.
// collection-map.ts
import { ProductSchema } from "./schema";
export const CollectionMap = {
products: ProductSchema,
} as const;
export type AppCollectionMap = typeof CollectionMap;3. Wrap your app with FirebaseProvider
Pass your initialized Firebase database, storage, and auth instances to the provider.
// main.tsx
import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";
import { getStorage } from "firebase/storage";
import { getAuth } from "firebase/auth";
import { FirebaseProvider } from "@manufac/firebase";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const firebaseConfig = {
apiKey: "...",
authDomain: "...",
projectId: "...",
storageBucket: "...",
messagingSenderId: "...",
appId: "...",
};
const app = initializeApp(firebaseConfig);
const database = getFirestore(app);
const storage = getStorage(app);
const auth = getAuth(app);
const queryClient = new QueryClient();
function App(): JSX.Element {
return (
<QueryClientProvider client={queryClient}>
<FirebaseProvider database={database} storage={storage} auth={auth}>
<YourApp />
</FirebaseProvider>
</QueryClientProvider>
);
}Firestore
Fetch a document by ID
import { useDocumentByID } from "@manufac/firebase";
import type { AppCollectionMap } from "./collection-map";
function ProductDetail({ productID }: { productID: string }): JSX.Element {
const { data: product, isPending } = useDocumentByID<AppCollectionMap, "products">(
"products",
productID,
);
if (isPending) {
return <p>Loading...</p>;
}
if (product === null || product === undefined) {
return <p>Product not found.</p>;
}
return <p>{product.name} — ${product.price}</p>;
}Fetch a list of documents (with optional pagination)
import { useDocuments } from "@manufac/firebase";
import { where } from "firebase/firestore";
import type { AppCollectionMap } from "./collection-map";
function AvailableProducts(): JSX.Element {
const {
data: products,
isPending,
activePage,
totalPages,
handleNextPage,
handlePreviousPage,
} = useDocuments<AppCollectionMap, "products">({
collectionName: "products",
queryConstraints: [where("isAvailable", "==", true)],
isPaginationEnabled: true,
pageSize: 10,
});
if (isPending) {
return <p>Loading...</p>;
}
return (
<div>
{products?.map((product) => (
<p key={product.id}>{product.name}</p>
))}
<p>
Page {activePage} of {totalPages}
</p>
<button onClick={handlePreviousPage} disabled={activePage === 1}>
Previous
</button>
<button onClick={handleNextPage} disabled={activePage === totalPages}>
Next
</button>
</div>
);
}Subscribe to real-time document updates
import { useRealtimeDocumentByID } from "@manufac/firebase";
import type { AppCollectionMap } from "./collection-map";
function LiveProduct({ productID }: { productID: string }): JSX.Element {
const product = useRealtimeDocumentByID<AppCollectionMap, "products">("products", productID);
if (product === null) {
return <p>Product not found.</p>;
}
return <p>{product.name} — ${product.price}</p>;
}Subscribe to real-time collection updates (with optional pagination)
import { useRealtimeDocuments } from "@manufac/firebase";
import type { AppCollectionMap } from "./collection-map";
function LiveProductList(): JSX.Element {
const { data: products, activePage, totalPages, handleNextPage, handlePreviousPage } =
useRealtimeDocuments<AppCollectionMap, "products">({
collectionName: "products",
isPaginationEnabled: true,
pageSize: 10,
});
return (
<div>
{products.map((product) => (
<p key={product.id}>{product.name}</p>
))}
<p>
Page {activePage} of {totalPages}
</p>
<button onClick={handlePreviousPage} disabled={activePage === 1}>
Previous
</button>
<button onClick={handleNextPage} disabled={activePage === totalPages}>
Next
</button>
</div>
);
}Delete a document
import { useDeleteDocumentByID } from "@manufac/firebase";
import type { AppCollectionMap } from "./collection-map";
function DeleteProductButton({ productID }: { productID: string }): JSX.Element {
const { mutate: deleteProduct, isPending } = useDeleteDocumentByID<AppCollectionMap>("products");
const handleClick = () => {
deleteProduct(productID);
};
return (
<button onClick={handleClick} disabled={isPending}>
{isPending ? "Deleting..." : "Delete"}
</button>
);
}Create a document (hook)
import { useCreateDocument } from "@manufac/firebase";
import type { AppCollectionMap } from "./collection-map";
function CreateProductButton(): JSX.Element {
const { mutate: createProduct, isPending } = useCreateDocument<AppCollectionMap, "products">("products");
const handleClick = () => {
createProduct({ name: "Widget", price: 9.99, isAvailable: true });
};
return (
<button onClick={handleClick} disabled={isPending}>
{isPending ? "Creating..." : "Create product"}
</button>
);
}To use a specific document ID, pass id in the data object. If a document with that ID already exists the call will throw.
createProduct({ id: "my-custom-id", name: "Widget", price: 9.99, isAvailable: true });Update a document
import { useUpdateDocument } from "@manufac/firebase";
import type { AppCollectionMap } from "./collection-map";
function ToggleAvailability({ productID }: { productID: string }): JSX.Element {
const { mutate: updateProduct, isPending } = useUpdateDocument<AppCollectionMap, "products">("products");
const handleClick = () => {
updateProduct({ id: productID, isAvailable: false });
};
return (
<button onClick={handleClick} disabled={isPending}>
{isPending ? "Updating..." : "Mark unavailable"}
</button>
);
}Get document count
import { useTotalDocumentCount } from "@manufac/firebase";
import type { AppCollectionMap } from "./collection-map";
function ProductCount(): JSX.Element {
const { data: count } = useTotalDocumentCount<AppCollectionMap>("products");
return <p>Total products: {count ?? 0}</p>;
}Firebase Storage
Upload a file
import { useUploadFile } from "@manufac/firebase";
function FileUploader(): JSX.Element {
const { mutate: uploadFile, isPending } = useUploadFile();
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file !== undefined) {
uploadFile({ path: `uploads/${file.name}`, data: file });
}
};
return <input type="file" onChange={handleChange} disabled={isPending} />;
}Get a download URL
import { useDownloadFileURL } from "@manufac/firebase";
function ProductImage({ path }: { path: string }): JSX.Element {
const { data: url } = useDownloadFileURL(path);
if (url === undefined) {
return <p>Loading image...</p>;
}
return <img src={url} alt="Product" />;
}Delete a file
import { useDeleteFile } from "@manufac/firebase";
function DeleteFileButton({ path }: { path: string }): JSX.Element {
const { mutate: deleteFile, isPending } = useDeleteFile();
const handleClick = () => {
deleteFile(path);
};
return (
<button onClick={handleClick} disabled={isPending}>
{isPending ? "Deleting..." : "Delete file"}
</button>
);
}Authentication
import { useAuth } from "@manufac/firebase";
function UserStatus(): JSX.Element {
const { user, isLoading, logout } = useAuth();
if (isLoading) {
return <p>Loading...</p>;
}
if (user === null) {
return <p>Not signed in.</p>;
}
return (
<div>
<p>Signed in as {user.email}</p>
<button onClick={logout}>Sign out</button>
</div>
);
}