Skip to content

Instantly share code, notes, and snippets.

@alexaivars
Last active January 13, 2022 10:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alexaivars/455397e9f1d84f1086613baff5263abe to your computer and use it in GitHub Desktop.
Save alexaivars/455397e9f1d84f1086613baff5263abe to your computer and use it in GitHub Desktop.
React hook for a curser based fires store document list that watches entire document collection
import {
useCallback,
useEffect,
useMemo,
useReducer,
useRef,
useState,
} from "react";
import {
QueryDocumentSnapshot,
Timestamp,
collection,
endAt,
getDocs,
getFirestore,
limit,
onSnapshot,
orderBy,
query,
startAfter,
DocumentChange,
} from "firebase/firestore";
export type DirektcenterAvatar = {
path: string;
};
export type DirektcenterAuthor = {
displayName: string;
role: string;
title?: string;
avatar?: DirektcenterAvatar;
};
export type DirektcenterPost = {
id: string;
createdAt: Timestamp;
body: string;
author: DirektcenterAuthor;
attachment?: any;
pinned: boolean;
};
const converter = {
toFirestore: (data: DirektcenterPost) => {
let ret = { ...data } as any;
delete ret.id;
return ret;
},
fromFirestore: (snap: QueryDocumentSnapshot) =>
({ id: snap.id, ...snap.data() } as DirektcenterPost),
};
function sortByCreatedAt(
a: [string, DirektcenterPost],
b: [string, DirektcenterPost]
) {
return b[1].createdAt.toMillis() - a[1].createdAt.toMillis();
}
function reducer(
state: Map<string, DirektcenterPost>,
action: DocumentChange<DirektcenterPost>
) {
const change = new Map(state);
switch (action.type) {
case "added": {
if (state.has(action.doc.id)) {
return state;
}
change.set(action.doc.id, action.doc.data());
break;
}
case "modified":
change.set(action.doc.id, action.doc.data());
break;
case "removed": {
change.delete(action.doc.id);
break;
}
default:
throw new Error(`Invalid DocumentChangeType ${action.type}`);
}
const posts = [...change]
.filter((entry) => !entry[1].pinned)
.sort(sortByCreatedAt);
const pinned = [...change]
.filter((entry) => entry[1].pinned)
.sort(sortByCreatedAt);
return new Map([...pinned, ...posts,]);
}
export default function useFirestorStream(collectionPath: string) {
const db = useRef(getFirestore());
const [lastVisibleDocument, setLastVisibleDocument] =
useState<QueryDocumentSnapshot<DirektcenterPost> | null>(null);
const [state, dispatch] = useReducer(
reducer,
new Map<string, DirektcenterPost>()
);
const [hasMore, setHasMore] = useState(false);
useEffect(() => {
const watch = query(
collection(db.current, collectionPath).withConverter(converter),
orderBy("pinned", "desc"),
orderBy("createdAt", "desc"),
lastVisibleDocument ? endAt(lastVisibleDocument) : limit(10)
);
const unsubscribe = onSnapshot(watch, (snapshot) => {
snapshot.docChanges().forEach((change) => {
dispatch(change);
});
if (lastVisibleDocument === null) {
setHasMore(snapshot.docs.length === 10);
setLastVisibleDocument(snapshot.docs[snapshot.docs.length - 1]);
}
});
return unsubscribe;
}, [collectionPath, lastVisibleDocument]);
const loadMore = useCallback(() => {
if (!lastVisibleDocument) {
return;
}
const runAsync = async () => {
const snapshot = await getDocs(
query(
collection(db.current, collectionPath).withConverter(converter),
orderBy("pinned", "desc"),
orderBy("createdAt", "desc"),
startAfter(lastVisibleDocument),
limit(10)
)
);
if (snapshot.empty) {
setHasMore(false);
return;
}
setHasMore(snapshot.docs.length === 10);
setLastVisibleDocument(snapshot.docs[snapshot.docs.length - 1]);
};
runAsync();
}, [lastVisibleDocument]);
return {
posts: useMemo(() => [...state.values()], [state]),
loadMore,
hasMore,
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment