Last active
August 11, 2020 18:36
-
-
Save gragland/383b0b77b4d05792c3a5a3c6e8a265af to your computer and use it in GitHub Desktop.
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
// 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); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment