Firebase
1. The Firestore Adapter
import {
collection,
query,
orderBy,
limit,
startAfter,
getDocs,
where,
QueryDocumentSnapshot,
Query,
DocumentData
} from "firebase/firestore";
import { db } from "../firebase-config"; // Your initialized Firestore db
import type { DataAdapter, QueryParams } from '@tablecraft/table';
export function createFirestoreAdapter<T>(
collectionName: string,
// We accept a reference to a mutable cursor object from the caller.
// This ensures SSR safety and instance-level isolation.
cursorState: {
lastVisibleDocs: Record<number, QueryDocumentSnapshot>;
lastParamKey?: string;
}
): DataAdapter<T> {
return {
async query(params: QueryParams) {
// Reset cursors if anything besides the page has changed
const paramKey = JSON.stringify({
filters: params.filters,
sort: params.sort,
sortOrder: params.sortOrder,
search: params.search
});
if (paramKey !== cursorState.lastParamKey) {
cursorState.lastVisibleDocs = {};
cursorState.lastParamKey = paramKey;
}
// 1. Base Query reference
let q: Query<DocumentData> = collection(db, collectionName);
// 2. Map Column Filtering (Where clauses)
if (params.filters) {
Object.entries(params.filters).forEach(([key, value]) => {
// Note: Firestore requires composite indexes for multiple where clauses
q = query(q, where(key, "==", value));
});
}
// 3. Map Sorting (Order By)
// Note: You MUST order by a field before using startAfter()
const sortField = params.sort || 'createdAt'; // Fallback if no sort selected
const sortDir = params.sortOrder || 'desc';
q = query(q, orderBy(sortField, sortDir));
// 4. Map Pagination (Cursor + Limit)
// If we are on page > 0, we need the cursor from the PREVIOUS page
if (params.page > 0) {
const lastVisible = cursorState.lastVisibleDocs[params.page - 1];
if (lastVisible) {
q = query(q, startAfter(lastVisible));
}
}
// Always apply the limit
q = query(q, limit(params.pageSize));
// 5. Execute the Query
const snapshot = await getDocs(q);
const data = snapshot.docs.map(doc => ({
id: doc.id,
...doc.data()
})) as T[];
// 6. Save the Cursor for the NEXT page
// Store the last document in our dictionary mapped to the CURRENT page index
if (snapshot.docs.length > 0) {
cursorState.lastVisibleDocs[params.page] = snapshot.docs[snapshot.docs.length - 1];
}
// 7. Return TableCraft QueryResult shape
// Notice we return `total: null` because Firestore doesn't provide it cheaply.
return {
data,
meta: {
total: null, // Unknown total count
page: params.page,
pageSize: params.pageSize,
totalPages: null, // Unknown total pages
countMode: 'estimated' // Tell TableCraft to use Next/Prev only pagination
}
};
}
};
}
// Usage in React:
// import { useRef, useMemo } from 'react';
//
// function MyTable() {
// // Keep the cursor state isolated to this specific component instance
// const cursorRef = useRef({ lastVisibleDocs: {} });
//
// const adapter = useMemo(() => {
// return createFirestoreAdapter<User>('users', cursorRef.current);
// }, []);
//
// return <DataTable adapter={adapter} />;
// }2. Important Notes on Firestore Pagination
Missing "Total Pages"
Sorting Limitations
Going Backwards
Last updated
Was this helpful?
