@manufac/firebase
v1.10.1
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.
Keywords
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>
);
}Tauri Google OAuth Setup
Prerequisites
- Make sure you have a Firebase project created.
- Make sure you have Google Auth configured in the Firebase console. Reference
Step 1: Getting Google Web Client ID
- Go to Firebase console
- Select your project
- Go to Authentication
- Go to Sign-in method
- Go to Google
- Go to Web SDK Configuration
- Copy the Web Client ID
Step 2: Allowing localhost as authorized redirect URI
Google OAuth requires localhost to be added as an authorized redirect URI. Since we spawn a local server, we need to add localhost to the authorized redirect URI.
- Go to Google Cloud Console
- Select your project
- Go to APIs & Services
- Go to Credentials
- Select your OAuth 2.0 Client ID (will be autogenerated by Google when you set up auth in your Firebase project)
- Add
http://localhostunder the Authorized Redirect URIs section. No need to specify a port number.
Usage
import { useTauriGoogleAuth } from "@manufac/firebase/tauri";
function SignInButton(): JSX.Element {
const { mutate: signInWithGoogle, isPending } = useTauriGoogleAuth({
googleWebClientID: "your-web-client-id",
});
const handleClick = () => {
signInWithGoogle();
};
return (
<button onClick={handleClick} disabled={isPending}>
{isPending ? "Signing in..." : "Sign in with Google"}
</button>
);
}