Last active
January 13, 2022 10:09
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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