// Usage function ProfilePage({ uid }) { // Subscribe to Firestore document const { data, status, error } = useFirestoreQuery( firestore.collection("profiles").doc(uid) ); if (status === "loading"){ return "Loading..."; } if (status === "error"){ return `Error: ${error.message}`; } return ( <div> <ProfileHeader avatar={data.avatar} name={data.name} /> <Posts posts={data.posts} /> </div> ); } // Reducer for hook state and actions const reducer = (state, action) => { switch (action.type) { case "idle": return { status: "idle", data: undefined, error: undefined }; case "loading": return { status: "loading", data: undefined, error: undefined }; case "success": return { status: "success", data: action.payload, error: undefined }; case "error": return { status: "error", data: undefined, error: action.payload }; default: throw new Error("invalid action"); } } // Hook function useFirestoreQuery(query) { // Our initial state // Start with an "idle" status if query is falsy, as that means hook consumer is // waiting on required data before creating the query object. // Example: useFirestoreQuery(uid && firestore.collection("profiles").doc(uid)) const initialState = { status: query ? "loading" : "idle", data: undefined, error: undefined }; // Setup our state and actions const [state, dispatch] = useReducer(reducer, initialState); // Get cached Firestore query object with useMemoCompare (https://usehooks.com/useMemoCompare) // Needed because firestore.collection("profiles").doc(uid) will always being a new object reference // causing effect to run -> state change -> rerender -> effect runs -> etc ... // This is nicer than requiring hook consumer to always memoize query with useMemo. const queryCached = useMemoCompare(query, prevQuery => { // Use built-in Firestore isEqual method to determine if "equal" return prevQuery && query && query.isEqual(prevQuery); }); useEffect(() => { // Return early if query is falsy and reset to "idle" status in case // we're coming from "success" or "error" status due to query change. if (!queryCached) { dispatch({ type: "idle" }); return; } dispatch({ type: "loading" }); // Subscribe to query with onSnapshot // Will unsubscribe on cleanup since this returns an unsubscribe function return queryCached.onSnapshot( response => { // Get data for collection or doc const data = response.docs ? getCollectionData(response) : getDocData(response); dispatch({ type: "success", payload: data }); }, error => { dispatch({ type: "error", payload: error }); } ); }, [queryCached]); // Only run effect if queryCached changes return state; } // Get doc data and merge doc.id function getDocData(doc) { return doc.exists === true ? { id: doc.id, ...doc.data() } : null; } // Get array of doc data from collection function getCollectionData(collection) { return collection.docs.map(getDocData); }