Skip to content

Instantly share code, notes, and snippets.

@gragland
Last active August 11, 2020 18:36
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 gragland/383b0b77b4d05792c3a5a3c6e8a265af to your computer and use it in GitHub Desktop.
Save gragland/383b0b77b4d05792c3a5a3c6e8a265af to your computer and use it in GitHub Desktop.
// 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