- Set up a new project on Firebase Console.
- Take a tour of firebase Console.
- Go the setup of your platform and copy config file in your codebase.
- Now Go to Database section in firebase console of your app and create a new Cloud Firestore.
- Follow necessary steps for ex. deciding for test mode(Development) or locked mode(Production).
- Put into test mode after selecting necessary location of the database server.
Let's make a new file called 'firebase.js'.
import firebase from "firebase/app";
// For Firebase JS SDK v7.20.0 and later, measurementId is optional
const firebaseConfig = {
apiKey: "YOUR_API_KEY",
authDomain: "YOUR_PROJECT_ID.firebaseapp.com",
databaseURL: "https://YOUR_PROJECT_ID.firebaseio.com",
projectId: "YOUR_PROJECT_ID",
storageBucket: "YOUR_PROJECT_ID.appspot.com",
messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
appId: "YOUR_APP_ID",
measurementId: "YOUR_MEASUREMENT_ID",
};
firebase.initializeApp(firebaseConfig);
export default firebase;Some Important Points:
- The apiKey just associates you with a Firebase project. We don't need to hide it.
- Your project will be protected by security rules later.
- There is a second, more important key that we'll use later that should be hidden.
- We're just pulling in
firebase/appso that we don't end up pulling in more than we need in our client-side application. - We configure Firebase and then we'll export it for use in other places in our application.
import firebase from "firebase/app";
import "firebase/firestore"; //New
// For Firebase JS SDK v7.20.0 and later, measurementId is optional
const firebaseConfig = {
apiKey: "YOUR_API_KEY",
authDomain: "YOUR_PROJECT_ID.firebaseapp.com",
databaseURL: "https://YOUR_PROJECT_ID.firebaseio.com",
projectId: "YOUR_PROJECT_ID",
storageBucket: "YOUR_PROJECT_ID.appspot.com",
messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
appId: "YOUR_APP_ID",
measurementId: "YOUR_MEASUREMENT_ID",
};
firebase.initializeApp(firebaseConfig);
export const firestore = firebase.firestore(); //NEW
export default firebase;Let's start by fetching posts whenever the 'Application' component by using useEffect.
First, let's pull in Cloud Firestore from our new firebase.js file.
import { firestore } from "../firebase";Now, we'll get all of the posts from Cloud Firestore whenever the Application component mounts by using useEffect.
useEffect(async () => {
const posts = await firestore.collection("posts").get();
console.log({ posts });
});Hmm.. that looks like a QuerySnapShot not our posts. What is that?
Actually Firebase Firestore returns us snapShot of the data. i.e current state
of the data in the database, there are two types of snapshots QuerySnapShot and DocumentSnapShot.
A QuerySnapShots has following properties:
docs: All the documents of the snapshotempty: This is a boolean that lets us know if the snapshot was empty.metadata: Metadata about this snapshot, concerning its source and if it has local modifications.- Example -
SnapShotMetadata {hasPendingWrites: false, fromCache: false}
- Example -
query: A reference to the query you fired.size: The number of documents in theQuerySnapShot.
and the following methods:
docChanges(): An array of changes since the last snapshot.forEach(): Iterates over the entire array of snapshots.isEqual(): Let's you know if it matches another snapshot.
References allow you to access the database itself. This is useful for getting the collection that document is from, deleting the document, listening for changes, setting and updating properties.
So now, let's iterate through all our documents. My Method:
useEffect(() => {
const getData = async () => {
const snapShot = firestore.collection("posts").onSnapshot((snapshot) => {
return snapshot.forEach((doc) => {
const id = doc.id;
const data = doc.data();
console.log({ id, data });
});
});
};
getData();
}, []);You should see results in the console.
Setting it to the setState to actually render some data in the app which we get from the application.
useEffect(() => {
const getData = async () => {
const snapShot = await firebase.collection("posts").get();
const posts = snapShot.docs.map((doc) => {
return { id: doc.id, ...doc.data() };
});
setState({ posts });
};
getData();
}, []);For sake of simplicity and reusability,
An aside, combining the document IDs with the data is something we're going to be doing a lot. Let's make a utility method in utilities.js:
export const collectIdsAndData = (doc) => ({ id: doc.id, ...doc.data() });Now, we will refactor our code as follows in App.js:
useEffect(() => {
const getData = async () => {
const snapShot = await firestore.collection("posts").get();
const posts = snapShot.docs.map(collectIdsAndData);
setState({ posts });
};
getData();
}, []);Now, we can get rid of predefined posts in state.
const [state, setState] = useState({
posts: [],
});First of all, we need to get rid of that Date.now() based id in AddPost. It was useful for us for a second or two there, but now have Firebase generating for us on our behalf.
// Function for Add Post.
const handleCreate = async (post){
const {posts} = state;
const docRef = await firestore.collection('posts').add(post);
const doc = await docRef.get();
const newPost = collectIdAndData(doc)
setState({posts: [...posts, newPost]})
}-> Get rid of automatically generate date-based ID!
In App.js
I will fix later but for now I am passing this function via props. from App.js => Posts.js => Post.js i.e Prop drilling.
const handleRemove = async (id) => {
const allPosts = state.posts;
await firestore.doc(`/posts/${id}`).delete();
const posts = allPosts.filter((post) => post.id !== id);
setState({ posts });
};Instead of managing data manually, you can also subscribe to the changes in the database. Instead of a get() method on the collection we can go with onSnapshot().
For realtime updates in the app use onSnapshot. Here we are continuously firing query so that data in the database change UI change. but we will stop listening to the database when component unmounted or navigate away from that section to prevent memory leaks,by using unsubscribe method.
refactoring useEffect in the App.js
useEffect(()=>{
let unsubscribe = null;
const getData = async ()=>{
unsubscribe = firestore.collection('posts').onSnapshot(snapshot=>{
const posts = snapshot.docs.map(collectIdAndData);
setState({posts});
})
getData();
// For unsubscribing from the database cleanup function
return ()=>{
unsubscribe();
}
})Currently I am manually passing data to the state, and firebase
does this automatically i will remove some code which are passed through props from App.js to their respective components.
In Post.jsx
// Grabbing id of the document and deleting them.
const postRef = firestore.doc(`posts/${id}`);
const remove = () => postRef.delete();
// Passing this remove function to the delete buttonIn AddPost.jsx
// Idk why doc() is working? will figure it out later.
firestore.collection("posts").doc().set(defaultPost);In App.jsx:
- Removed the
handleCreatemethod completely. - Removed the
handleRemovemethod completely. - Removed
onCreateandonRemovefrom the<Post />component in therender()method.
useEffect(()=>{
let unsubscribe = null;
const getData = async ()=>{
unsubscribe = firestore.collection('posts').orderBy('createdAt','desc').onSnapshot(snapshot=>{
const posts = await snapshot.docs.map(collectIdAndData);
setState({posts})
})
}
getData();
return ()=>{
unsubscribe();
}
},[])Let's implement a approach to updating documents in cloud firestore.
For updating, we can use set() method which will create new record, if data not exists otherwise wipe the existing data.
Another method is update() which will update the some fields of the document without overwriting the existing data.
In Blog Post, we have star button, so when a user clicks on the star button, we should eventually increase the stars in the post.
Now currently is not the best way to do this, but I will update it later.
const postRef = firestore.doc(`posts/${id}`);
const addStar = () => postRef.update({ stars: stars + 1 });
// Now passing this function to the star button by using onClick event.Right now, the application is wide open. If we pushed this to production, any user could do literally anything they wanted with our database. That's not good.
Let's implement authentication in our application.
First, let's head over to the dashboard and turn on some authentication. We'll be using 4 forms of authentication.
- Email and password authentication
- Google sign-in
- Github sign-in
- Facebook sign-in
Let's enable this on firebase console.
Google authentication will not require to register our app explicitly.
but for github and facebook sign-in we will have to register our app in their consoles and from there we will get App-Id and App-secret, which you can store in the sign-in-method tab of the authentication in firebase console.
const [user, setUser] = useState({
user: null,
});Cool.We have a CurrentUser, SignIn, and SignUp components ready to rock.
We're going to start with Google Sign-in because I can assume you have a Google account if you can create a Firebase application.
In firebase.js:
import "firebase/auth";
// ...
export const auth = firebase.auth();
export const provider = new firebase.auth.GoogleAuthProvider();
// there is signInWithRedirect method also which will redirect to the google sign in page.
export const googleSignIn = () => auth.signInWithPopup(provider);In SignIn.jsx
<button onClick={googleSignIn}>Sign In With Google</button>same goes with facebook and github sign in.
-
create new the provider with respective sign in.
-
i.e export const provider = new firebase.auth.FacebookAuthProvider()
-
create the method to pass the provider in the signInWithPopup or signInWithRedirect function.
-
i.e export const facebookSignIn = () => auth.signInWithPopup(provider)
In App.js
useEffect(() => {
let unsubscribeFromFirestore = null;
let unsubscribeFromAuth = null;
const getData = async () => {
unsubscribeFromFirestore = await firestore
.collection("posts")
.orderBy("createdAt", "desc")
.onSnapshot((snapshot) => {
const posts = snapshot.docs.map(collectIdAndData);
setState({ posts });
});
};
const getAuth = async () => {
// OnAuthStateChanged method is for when user login, we will get user information and when user logout then user will set back to null.
unsubscribeFromAuth = await auth.onAuthStateChanged((user) => {
setState({ user });
});
};
getData();
getAuth();
return () => {
unsubscribeFromFirestore();
unsubscribeFromAuth();
};
}, []);In firebase.js
export const signOut = () => auth.signOut();Now In CurrentUser.jsx
<button onClick={signOut}>Sign Out</button>Up until now, everything has been wide open. That's not great. If we are going to push stuff to production, we're going to need to start adding some security to our application.
Cloud Firestore rules always following this structure:
service.cloud.firestore {
match /databases/{database}/documents{
// ..
}
}There is a nice query pattern for rules:
service.cloud.firestore {
match /databases/{database}/documents {
match /posts/{postId} {
allow read: if <Condition>;
allow write: if <Condition>;
}
}
}You can combine them into one:
service.cloud.firestore {
match /databases/{database}/documents{
match /posts/{postId}{
allow read, create, update: if <condition>;
}
}
}You can get a bit more granular if you'd like:
read- Applies to both lists and documents.get- When reading a single document.list- When querying a collection.
write- Applies rule to create, update, and delete.create- When setting new data withdocRef.set()orcollectionRef.add()update- When updating data withdocRef.update()orset()delete- When deleting data withdocRef.delete()
You can nest rules to sub-collections:
service.cloud.firestore {
match /databases/{database}/documents {
match /posts/{postId}{
match /comments/{comment}{
allow read, write: if <condition>;
}
}
}
}If you want to go to arbitrary depth, then you can do {document=**}.
Important: If multiple requests match, then the operation is allowed if any of them is true.
resource.datawill have the fields on the document as it stored in the database.request.resource.datawill having the incoming document. (Note: This is all you have if you're responding to document creation.)
Only read or write if you are logged in.
service.cloud.firestore {
match /databases/{database}/document{
// Allow the user to access documents in the "posts" collection
// only if they are authenticated
match /posts/{postId} {
allow read, write: if request.auth.id != null;
}
}
}Secure by owner, Has-one Relationship
Only read and write your own data:
service.cloud.firestore{
match /databases/{database}/document{
match /users/{userId} {
allow read, update, delete: if belongsTo(userId);
allow create: if request.auth.uid != null;
function belongsTo(userId){
return request.auth.uid == userId;
}
}
}
}Create a rule that insists on title
service.cloud.firestore{
match /databases/{database}/document{
match /posts/{postId}{
allow read;
allow create: if request.auth.uid != null && !request.resource.data.title;
allow update, delete: if request.auth.uid == resource.data.user.uid;
}
}
}Sometimes a user will own many documents in a collection, so the Document ID will be different than the User ID. In this case, we can look at the request (create) and or the existing resource (delete), assuming it has a uid property to track the relationship. Example: user has-many posts.
service.cloud.firestore{
match /databases/{database}/document{
match /posts/{postId}{
allow write: if requestMatchesUID();
allow update: if requestMatchesUID() && resourceMatchesUID();
allow delete: if resourceMatchesUID();
}
function requestMatchesUID(){
return request.auth.uid == request.resource.data.uid;
}
function resourceMatchesUID(){
return request.auth.uid == request.data.uid;
}
}
}Make all Collections Readable or Writable - Except One
Let’s imagine you create collection names dynamically and want them to be unlocked by default. However, you have a special collection that requires strict rules. You start by locking down all paths, then dynamically pass the collection name in a rule. If the name does not equal the special collection then allow the operation.
service.cloud.firestore{
match /{document=**}{
allow read, write: if false;
}
match /{collectionName}/{docId}{
allow read: if collectionName != 'special-collection';
}
}service cloud.firestore {
match /databases/{database}/documents {
function isSignedIn() {
return request.auth != null;
}
function emailVerified() {
return request.auth.token.email_verified;
}
function userExists() {
return exists(/databases/$(database)/documents/users/$(request.auth.uid));
}
// [READ] Data that exists on the Firestore document
function existingData() {
return resource.data;
}
// [WRITE] Data that is sent to a Firestore document
function incomingData() {
return request.resource.data;
}
// Does the logged-in user match the requested userId?
function isUser(userId) {
return request.auth.uid == userId;
}
// Fetch a user from Firestore
function getUserData() {
return get(/databases/$(database)/documents/accounts/$(request.auth.uid)).data
}
// Fetch a user-specific field from Firestore
function userEmail(userId) {
return get(/databases/$(database)/documents/users/$(userId)).data.email;
}
// example application for functions
match /orders/{orderId} {
allow create: if isSignedIn() && emailVerified() && isUser(incomingData().userId);
allow read, list, update, delete: if isSignedIn() && isUser(existingData().userId);
}
}
}exists(/databases/$(database)/documents/users/$(request.auth.uid))will verify that a document exists.get(/databases/$(database)/documents/users/$(request.auth.uid)).datawill get you the data of another document.
Now let’s combine some of the functions created earlier to build a robust validation rule. By chaining together rules with && we can validate the data structure of multiple fields as an AND condition. We can also use || for OR conditions.
// allow update: if isValidProduct();
function isValidProduct() {
return (
incomingData().price > 10 &&
incomingData().name.size() < 50 &&
incomingData().category in ["widgets", "things"] &&
existingData().locked == false &&
getUserData().admin == true
);
}- You can limit the size of a query so that malicious users (or you after a big lunch) can't run expensive queries
allow list: if request.query.limit <= 10;
Firestore also includes a duration helper to generate dates that can be operated upon. For example, we might want to throttle updates to 1 minute intervals. We can create this rule by comparing the request.time to a timestamp on the document + the throttle duration.
// allow update: if isThrottled() == false;
function isThrottled() {
return request.time < resource.data.lastUpdate + duration.value(1, "m");
}In SignUp.jsx
const handleSubmit = async (event) => {
event.preventDefault();
const { displayName, email, password } = signUpState;
try {
const { user } = await auth.createUserWithEmailAndPassword(email, password);
user.updateProfile({ displayName });
} catch (error) {
console.error(error.message);
}
setSignUpState({ displayName: "", email: "", password: "" });
};Now this has some problems
- The display name would not update immediately, because see in try block we are creating user with passing email and password first and then we are updating profile of user with
displayName; - There is not
photoURLwhich we are getting free from google signIn or facebook signIn. - we may want to store other information beyond what we get from user profile.
Now what we can do? we can create documents for user profile in Cloud Firestore.
The information on the user object seems great, but we going to run into limitations real quick.
- What if we want to let the user set bio or something?
- What if we want to set admin permissions on the users?
- What if we want to keep track of posts a user has liked?
There are many more possibilities, right?
The solution is, we will make documents based off the users uid in Cloud firestore.
In firebase.js
export const createUserProfileDocument = async (user, additionalData) => {
// If there is no user, let's not create his document.
if (!user) return;
// Getting a reference to the location in the firestore where the user document may or may not exist.
const userRef = firestore.doc(`users/${user.uid}`);
// Go and fetch document from that location
const snapshot = await userRef.get();
// If there is not a document for the user. Let's use information that we got from either Google or our sign in form.
if (!snapshot.exists) {
const { displayName, email, photoURL } = user;
const createdAt = new Date().toUTCString();
try {
await userRef.set({
displayName,
email,
createdAt,
...additionalData,
});
} catch (error) {
console.error("Error Creating User", error.message);
}
}
// Get the document and return it, since that's we are likely want to do next.
return getUserDocument(user.uid);
};
export const getUserDocument = async (uid) => {
if (!uid) return null;
try {
// Getting uid of the users document
const userDocument = await firestore.collection("users").doc("uid").get();
// Returning uid and all the saved data in the user document.
return { uid, ...userDocument.data() };
} catch (error) {
console.error("Error Fetching User", error.message);
}
};I am going to put it into two places:
onAuthStateChangedin order to get google sign Ups- In
handleSubmitinSignUpbecause there's we will display custom displayName.
match /users/{userId} {
allow read;
allow write: if request.auth.uid == userId;
}We have a small bug. For our first-time email users, we'll still get null for their display name.
We could solve all of this by passing everything down from the Application component, but I feel like we might be able do a little better.
We could wrap everything in HOCs, but that might also end us up in a position where we make additional queries to Cloud Firestore. This isn't ideal, but it's probably not the biggest problem in the world.
We could use something big like Redux. But that for some other project.
But for this, I am using Context API. As currently firebase does our actions in the state, that's why I am not using any useReducer, but who knows in future I will.
In PostProvider.jsx
import React, { createContext, useState, useEffect } from "react";
import { firestore } from "../firebase";
import { collectIdAndData } from "../utilities";
export const PostContext = createContext();
const PostProvider = ({ children }) => {
const [state, setState] = useState({
posts: [],
});
useEffect(() => {
let unsubscribeFromFirestore = null;
const getData = async () => {
unsubscribeFromFirestore = await firestore
.collection("posts")
.orderBy("createdAt", "desc")
.onSnapshot((snapshot) => {
const posts = snapshot.docs.map(collectIdAndData);
setState({ posts });
});
};
getData();
return () => {
unsubscribeFromFirestore();
};
}, []);
const { posts } = state;
return <PostContext.Provider value={posts}>{children}</PostContext.Provider>;
};
export default PostProvider;Hooking up the Post Provider
In index.js:
import PostsProvider from "./contexts/PostsProvider";
ReactDOM.render(
<PostsProvider>
<App />
</PostsProvider>,
document.getElementById("root")
);Now in Post.jsx
import React, { useContext } from "react";
import { PostContext } from "../Context/PostsProvider";
import AddPost from "./AddPost";
import Post from "./Post";
function Posts() {
const posts = useContext(PostContext);
return (
<section className="posts">
<AddPost />
{posts.map((post) => (
<Post {...post} id={post.id} key={post.id} />
))}
</section>
);
}
export default Posts;Similarly for our User's state,
In UserProvider.jsx
import React, { createContext, useState, useEffect } from "react";
import { auth, createUserProfileDocument } from "../firebase";
export const UserContext = createContext();
const UserProvider = ({ children }) => {
const [state, setState] = useState({
user: null,
userLoaded: true,
});
useEffect(() => {
let unsubscribeFromAuth = null;
const getAuth = async () => {
unsubscribeFromAuth = auth.onAuthStateChanged(async (userAuth) => {
const user = await createUserProfileDocument(userAuth);
// console.log(user);
setState({ user, userLoaded: false });
});
};
getAuth();
return () => {
unsubscribeFromAuth();
};
}, []);
const { user, userLoaded } = state;
return (
<UserContext.Provider value={{ user, userLoaded }}>
{children}
</UserContext.Provider>
);
};
export default UserProvider;Now similarly wrapping UserProvider component in index.js
import React from "react";
import ReactDOM from "react-dom";
import "./index.scss";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
import PostProvider from "./Context/PostsProvider";
import UserProvider from "./Context/UserProvider";
ReactDOM.render(
<React.StrictMode>
<PostProvider>
<UserProvider>
<App />
</UserProvider>
</PostProvider>
</React.StrictMode>,
document.getElementById("root")
);Now in Authentication.js
import React, { useContext } from "react";
import SignInAndSignUp from "./SignInAndSignUp";
import CurrentUser from "./CurrentUser";
import { UserContext } from "../Context/UserProvider";
function Authentication() {
const { user, userLoaded } = useContext(UserContext);
if (userLoaded) return null;
// console.log(user);
return <div>{user ? <CurrentUser {...user} /> : <SignInAndSignUp />}</div>;
}
export default Authentication;Now let's implement code to hide the delete button if it is not your post. In security rules also we had wrote rules so that the original user can edit and delete their post.
And because of context API we can reach to our state whichever from component we want.
In Post.jsx
// Importing state from `UserProvider`
const currentUser = useContext(UserProvider);
// Here I am creating a little helper function
const belongsToCurrentUser = (currentUser, postAuthor) => {
if (!currentUser) return false;
return currentUser.uid === postAuthor.uid;
};
// Now in delete button
{
belongsToCurrentUser(currentUser.user, user) && (
<button className="delete" onClick={remove}>
Delete
</button>
);
}This is the page where we can edit profile photos and displayName.
Here I am implementing React Router. So for setup:
In index.js
import {BrowserRouter as Router} from 'react-router-dom'
ReactDOM.render(
<Router>
<UserProvider>
<PostProvider>
<App/>
</PostProvider>
</UserProvider>
</Router>,
document.getElementById('root');
);In App.js
import React from "react";
import Authentication from "./components/Authentication";
import Posts from "./components/Posts";
import { Switch, Route, Link } from "react-router-dom";
import UserProfile from "./UserProfile";
function App() {
return (
<main className="Application">
<Link className="links" to="/">
<h1>My Blogger</h1>
</Link>
<Authentication />
<Switch>
<Route exact path="/profile" component={UserProfile}></Route>
<Route exact path="/" component={Posts}></Route>
</Switch>
</main>
);
}
export default App;In CurrentUser.jsx
<Link to="/profile">
<h2>{displayName}</h2>
</Link>Okay, let's implement UserProfile page
import { auth } from "./firebase";
import React, { useRef, useState } from "react";
import { firestore } from "./firebase";
const UserProfile = () => {
const [state, setState] = useState({
displayName: "",
});
const imageInput = useRef(null);
const handleChange = (event) => {
const { name, value } = event.target;
setState({ [name]: value });
};
const uid = () => {
return auth.currentUser.uid;
};
const userRef = () => {
return firestore.doc(`users/${uid()}`);
};
const handleSubmit = (event) => {
event.preventDefault();
const { displayName } = state;
if (displayName) {
userRef().update({
displayName,
});
}
};
const { displayName } = state;
return (
<section className="UserProfile">
<form onSubmit={handleSubmit} className="UpdateUser">
<input
type="text"
name="displayName"
value={displayName}
onChange={handleChange}
placeholder="Enter display name."
/>
// Asking myself again why I have written ref like this, maybe to set and
get file name from the input
<input
type="file"
ref={(ref) => (imageInput.current = ref)}
name="image-upload"
/>
<input className="update" type="submit" />
</form>
</section>
);
};
export default UserProfile;So what if user wants to upload a new profile picture ?
We should facilitate that, right ?
So by using firebase storage, we will store images in it. By this way, we can upload new profile picture for those users who will sign in from email and password and can re-upload profile picture who sign in from googleSignIn or other OAuth providers.
Let's add storage to firebase.js
import "firebase/storage";Cool, now we will export that as well
export const storage = firebase.storage();Now Comes the meat part, uploading the file
back in UserProfile.js:
const imageInput = useRef(null);
// Creating function for getting filename from the input field if it exists and only getting one file at a time.
const file = () => {
return imageInput.current && imageInput.current.files[0];
};
// Now in handleSubmit()
if (file()) {
storage
.ref()
.child("user-profiles")
.child(uid())
.child(file().name)
.put(file())
.then((response) => response.ref.getDownloadURL())
.then((photoURL) => userRef().update({ photoURL }));
}Basically it is checking if the file exists if exists then we are referencing the storage by storage.ref(), then we are creating a new folder named user-profiles and then inside it we are creating a new folder of uid or userId referenced to the user who has signed in currently and then putting that file into that folder with that same name in which we have selected from our file storage.
storage.ref return promise so in that we are getting url of the file by calling getDownloadURL() method.
We are also updating our database by adding photoURL to the users document.
service.firebase.storage {
match /b/{bucket}/o {
match /user-profiles/{userId}/{photoURL}{
allow read, write: if request.auth.uid == userId;
}
}
}Basically meaning a collection under a collection i.e nested collections Suppose our posts collection have a comments sub-collection, then those comments are sub-collection of only individual posts.
Each comments in the one post is unique than the other comment in the another post.
Advantages:
-
It gives you more structured database.
-
Queries are indexed by default. Query Performance is proportional to the size of your result set, not your data set.
So does not matter the size of your application, the performance depends on the size of your result set. -
Each document has max size of 1MB.For instance, if you have an array of orders in your customer document, it might be a good idea to create a subcollection of orders to each customer because you cannot foresee how many orders a customer will have. By doing this you don’t need to worry about the max size of your document. -
Pricing: Firestore charges you for document reads, writes and deletes. Therefore, when you create many subcollections instead of using arrays in the documents, you will need to perform more read, writes and deletes, thus increasing your bill. -
Documents are easier to delete. Using sub collections you need to make sure to first delete all sub collection documents before you delete the parent document. There is no API for this so you might need to roll your own helper functions.
-
Having the parent id directly in each (sub) document might make it easier to process query results, depending on the application.
-
No need to store a reference/foreign key/id of the parent document, as it is implied by the database structure. You can get to the parent via the sub collection document ref.
Let's create a page for a single post where one can leave comments
In PostPage.jsx:
import React, { useEffect, useState } from "react";
import Post from "./Post";
import Comments from "./Comments";
import { firestore } from "../firebase";
import { collectIdAndData } from "../utilities";
import { withRouter } from 'react-router-dom';
const PostPage = (props)=>{
const [post, setPost] = useState(null);
const [comments, setComments] = useState([]);
// Some Helper functions.
const postId = ()=>{
return props.match.params.id;
};
const postRef = ()=>{
return firestore.doc(`/posts/${postId()}`);
}
const commentsRef = ()=>{
const commentRefs = postRef().collection("comments");
return commentRefs;
}
useEffect(()=>{
let unsubscribeFromComment = null;
let unsubscribeFromPost = null;
const getPost = async () =>{
unsubscribeFromPost = await postRef().onSnapshot(snapshot=>{
const posts = collectIdAndData(snapshot);
setPost(posts)
});
};
const getComments = ()=>{
unsubscribeFromComment = commentRef().onSnapShot(snapshot=>{
const comments = snapshot.docs.map(collectIdAndData);
setComments(comments);
});
};
getPost();
getComments();
return ()=>{
unsubscribeFromPost();
unsubscribeFromComment();
}
}, []);
return (
<section>
{post && <Post {...post}>}
<Comments comments={comments}/>
</section>
)
}
export default withRouter(PostPage);So what I have done here is grab that prop, hook into the firebase, subscribe to its document, also get its comments which is a subcollection and subscribe to those as well and hold on the references so that we can add comments there, right! which will be determined by the URL which will tell us what to subscribe to and unsubscribe to.