Introduction
In this tutorial we will add Firebase to a Next.js website. If you are using another react based framework, the process should be very similar and you should get something out of this tutorial.
We will first connect the website to Firebase (which is a step in itself), and then we'll add the following functionalities in the following order:
Initialize Firebase
First, make sure you have a Firebase project. You can create one here if you do not have one.
Create a folder called firebase
and inside of that folder create a file called initFirebase.js
. We will add
all our firebase related functionality inside of this folder.
Now, install the 2 dependancies:
yarn add firebase firebase-admin
Import these inside of initFirebase.js
.
If you haven't already, create a web app inside of your Firebase project. Once you go through those steps, you will be able to download your credentials. From here, create a file called .env and paste in the secrets. Your file should look like mine (Except with your secret variables obviously!).
NEXT_PUBLIC_FIREBASE_API_KEY=
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=
NEXT_PUBLIC_FIREBASE_DATABASE_URL=
NEXT_PUBLIC_FIREBASE_PROJECT_ID=
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=
NEXT_PUBLIC_FIREBASE_APP_ID=
# for firebase-admin
FIREBASE_CLIENT_EMAIL=
FIREBASE_PRIVATE_KEY=
We can now use these secrets to initialize Firebase. Your initFirebase
file should look like this:
import firebase from 'firebase/app'
// the below imports are option - comment out what you don't need
import 'firebase/auth'
import 'firebase/firestore'
import 'firebase/storage'
import 'firebase/analytics'
import 'firebase/performance'
const clientCredentials = {
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
databaseURL: process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL,
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
}
Lastly, let's export a function called initFirebase()
that uses these credentials to initialize Firebase!
export default function initFirebase() {
if (!firebase.apps.length) {
firebase.initializeApp(clientCredentials)
// Check that `window` is in scope for the analytics module!
if (typeof window !== 'undefined') {
// Enable analytics. https://firebase.google.com/docs/analytics/get-started
if ('measurementId' in clientCredentials) {
firebase.analytics()
firebase.performance()
}
}
console.log('Firebase was successfully init.')
}
}
Now that Firebase is initialized, we can begin to use it.
Cloud Firestore
We will start off with cloud Firestore.
Write
Create a folder named components. Inside of this folder, create a folder called
cloudFirestore
. All of our cloud Firestore related components will go inside of here.
Inside of here, create a file named write.js
. In here we will:
- Import Firebase
- Import cloud Firestore
Then, create a basic React component named WriteToCloudFirestore
. Your file should look like this:
import firebase from 'firebase/app'
import 'firebase/firestore'
const WriteToCloudFirestore = () => {
return (
<div style={{ margin: '5px 0' }}>
</div>
)
}
export default WriteToCloudFirestore
To send data, we need to create a function sendData
. We will then invoke this function on a button press.
We'll wrap it in a try catch loop as well.
import firebase from 'firebase/app'
import 'firebase/firestore'
const WriteToCloudFirestore = () => {
const sendData = () => {
try {
// write data here
} catch (error) {
console.log(error)
alert(error)
}
}
return (
<div style={{ margin: '5px 0' }}>
<button onClick={sendData}>Send Data To Cloud Firestore</button>
</div>
)
}
export default WriteToCloudFirestore
To write data, we access the firestore method on the firebase object. We grab the collection called myCollection
(can be any name you like),
we grad the document called my_document
(can be any name you like), and then use the set()
method. It looks something like this:
firebase
.firestore()
.collection('myCollection')
.doc('my_document') // leave as .doc() for a random unique doc name to be assigned
.set({
string_data: 'Benjamin Carlson',
number_data: 2,
boolean_data: true,
map_data: { stringInMap: 'Hi', numberInMap: 7 },
array_data: ['text', 4],
null_data: null,
time_stamp: firebase.firestore.Timestamp.fromDate(new Date('December 17, 1995 03:24:00')),
geo_point: new firebase.firestore.GeoPoint(34.714322, -131.468435)
})
.then(alert('Data was successfully sent to cloud firestore!'))
When we put it all together it looks like this:
import firebase from 'firebase/app'
import 'firebase/firestore'
const WriteToCloudFirestore = () => {
const sendData = () => {
try {
firebase
.firestore()
.collection('myCollection')
.doc('my_document') // leave as .doc() for a random unique doc name to be assigned
.set({
string_data: 'Benjamin Carlson',
number_data: 2,
boolean_data: true,
map_data: { stringInMap: 'Hi', numberInMap: 7 },
array_data: ['text', 4],
null_data: null,
time_stamp: firebase.firestore.Timestamp.fromDate(new Date('December 17, 1995 03:24:00')),
geo_point: new firebase.firestore.GeoPoint(34.714322, -131.468435)
})
.then(alert('Data was successfully sent to cloud firestore!'))
} catch (error) {
console.log(error)
alert(error)
}
}
return (
<button onClick={sendData}>Send Data To Cloud Firestore</button>
)
}
export default WriteToCloudFirestore
Imort this component inside of index.js
and press the button. You should see a success alert
and if you navigate to Firbase you should see the data.
Read
Now let's read this data. Create a file named Read.js
in the same folder as Write.js
.
Make a component named ReadDataFromCloudFirestore()
. Inside of it create a button, with
an onClick event which invokes a function named readData
. Your file should look like this:
import firebase from 'firebase/app'
import 'firebase/firestore'
const ReadDataFromCloudFirestore = () => {
const readData = () => {
}
return (
<button onClick={readData}>Read Data From Cloud Firestore</button>
)
}
export default ReadDataFromCloudFirestore
Now, inside of the method lets read the data by invoking the firestore() method off the firebase
object. Select the collection and the document the same way as before. Next, use the onSnapshot
method. Inide of it, create a function and console.log the data. The data is inside of doc.data(). The doc
object is the entire response. Here is the complete file:
import firebase from 'firebase/app'
import 'firebase/firestore'
const ReadDataFromCloudFirestore = () => {
const readData = () => {
try {
firebase
.firestore()
.collection('myCollection')
.doc('my_document')
.onSnapshot(function (doc) {
console.log(doc.data())
})
alert('Data was successfully fetched from cloud firestore! Close this alert and check console for output.')
} catch (error) {
console.log(error)
alert(error)
}
}
return (
<button onClick={readData}>Read Data From Cloud Firestore</button>
)
}
export default ReadDataFromCloudFirestore
Import this function into index.js
and you'll see your data in the console!
Authentication
Popup Auth Flow
Create a folder called Auth
inside of your components folder. Inside of there,
create a file named FirebaseAuth.js
. We will be using the sign in pop up flow from the ract firebase ui
library. We'll start by importing what we need.
import initFirebase from '../../firebase/initFirebase'
import { useEffect, useState } from 'react'
import StyledFirebaseAuth from 'react-firebaseui/StyledFirebaseAuth'
import firebase from 'firebase/app'
import 'firebase/auth'
import { setUserCookie } from '../../firebase/userCookies'
import { mapUserData } from '../../firebase/mapUserData'
Note, we have not created the setUserCookie
or setUserCookie
files yet.
You might notice that we are importing initFirebase
when we already have it in the index.js
file. This is
because it makes more sense to init Firebase in the auth flow rather than the index page because if you are building out
a large application, the user will not always start at the index page.
Next, paste the code below:
initFirebase() // initialize firebase
const firebaseAuthConfig = {
signInFlow: 'popup',
// Auth providers
// https://github.com/firebase/firebaseui-web#configure-oauth-providers
signInOptions: [
{
provider: firebase.auth.EmailAuthProvider.PROVIDER_ID,
requireDisplayName: false,
},
// add additional auth flows below
firebase.auth.GoogleAuthProvider.PROVIDER_ID,
firebase.auth.TwitterAuthProvider.PROVIDER_ID,
firebase.auth.GithubAuthProvider.PROVIDER_ID,
],
signInSuccessUrl: '/',
credentialHelper: 'none',
callbacks: {
signInSuccessWithAuthResult: async ({ user }, redirectUrl) => {
const userData = mapUserData(user)
setUserCookie(userData)
},
},
}
That is how easy it is to create a popup auth flow. You will notice that I provide multiple sign in options and set user data and cookies, which again, we have yet to implement.
Finally, create the actual component and return this config inside of the pop up auth flow that we imported from react ui.
const FirebaseAuth = () => {
// Do not SSR FirebaseUI, because it is not supported.
// https://github.com/firebase/firebaseui-web/issues/213
const [renderAuth, setRenderAuth] = useState(false)
useEffect(() => {
if (typeof window !== 'undefined') {
setRenderAuth(true)
}
}, [])
return (
<div>
{renderAuth ? (
<StyledFirebaseAuth
uiConfig={firebaseAuthConfig}
firebaseAuth={firebase.auth()}
/>
) : null}
</div>
)
}
export default FirebaseAuth
Inside of our pages folder, create a file named auth.js
and add the component we just created.
import FirebaseAuth from '../components/auth/FirebaseAuth'
const Auth = () => {
return (
<div>
<div>
<FirebaseAuth />
<p><a href="/">Go Home</a></p>
</div>
</div>
)
}
export default Auth
While we are in the pages folder, remove the initFirebase line from index.js
as well.
Auth Logic
The only stuff left to do now is handle the logic. Create 4 files inside of your firebases folder.
- mapUserData.js
- useUser.js
- userCookies.js
Cookies
Paste in the following code into userCookies.js
. This is taken from the next.js documentation.
import cookies from 'js-cookie'
export const getUserFromCookie = () => {
const cookie = cookies.get('auth')
if (!cookie) {
return
}
return JSON.parse(cookie)
}
export const setUserCookie = (user) => {
cookies.set('auth', user, {
// firebase id tokens expire in one hour
// set cookie expiry to match
expires: 1 / 24,
})
}
export const removeUserCookie = () => cookies.remove('auth')
This code sets cookies so the user can close the website and reopen it and not have to log back in. Make sure to add js-cookie
via yarn or npm.
User Data
Next, inside of map user data, add the following:
export const mapUserData = (user) => {
const { uid, email, xa, displayName, photoUrl } = user
return {
id: uid,
email,
token: xa,
name: displayName,
profilePic: photoUrl
}
}
This again is taken from the Next.js website. The code maps the data we are getting to out custom variables.
Custom User Hook
Finally, let's modify the useUser
file. This file has the following tasks:
- create a log out method.
- gives us a hook to get the logged in user anywhere in out app.
- provides an auth listener
Here is the code:
import { useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import firebase from 'firebase/app'
import 'firebase/auth'
import initFirebase from '../firebase/initFirebase'
import {
removeUserCookie,
setUserCookie,
getUserFromCookie,
} from './userCookies'
import { mapUserData } from './mapUserData'
initFirebase()
const useUser = () => {
const [user, setUser] = useState()
const router = useRouter()
const logout = async () => {
return firebase
.auth()
.signOut()
.then(() => {
// Sign-out successful.
router.push('/auth')
})
.catch((e) => {
console.error(e)
})
}
useEffect(() => {
// Firebase updates the id token every hour, this
// makes sure the react state and the cookie are
// both kept up to date
const cancelAuthListener = firebase.auth().onIdTokenChanged((user) => {
if (user) {
const userData = mapUserData(user)
setUserCookie(userData)
setUser(userData)
} else {
removeUserCookie()
setUser()
}
})
const userFromCookie = getUserFromCookie()
if (!userFromCookie) {
router.push('/')
return
}
setUser(userFromCookie)
return () => {
cancelAuthListener()
}
}, [])
return { user, logout }
}
export { useUser }
The auth should now work. Try to log in with email and password. Note, if you want to use any of the OAuth providers you will need to turn them on in your firebase auth settings page.
Now that we have auth, we want to dynamically show a custom index page based on if the user is logged in or not.
Make the following changes to index.js
.
import Head from 'next/head'
import styles from '../styles/Home.module.css'
import WriteToCloudFirestore from '../components/cloudFirestore/Write'
import ReadDataFromCloudFirestore from '../components/cloudFirestore/Read'
import { useUser } from '../firebase/useUser'
export default function Home() {
const { user, logout } = useUser()
if (user) {
return (
<div>
<h1>{user.name}</h1>
<h3>{user.email}</h3>
{user.profilePic ? <image src={user.profilePic} height={50} width={50}></image> : <p>No profile pic</p>}
<WriteToCloudFirestore />
<ReadDataFromCloudFirestore />
<button onClick={() => logout()}>Log Out</button>
</div>
)
}
else return (
<div className={styles.container}>
<p><a href="/auth">Log In!</a></p>
<Head>
<title>Create Next App</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<main className={styles.main}>
<h1 className={styles.title}>
Welcome to <a href="https://nextjs.org">Next.js!</a>
</h1>
// the rest is the same
Updating Read/Write
Finally, let's change the document name when we send data via cloud firestore. This way, each user has their data stored in a unique key.
In cloudFirestore
folder:
import firebase from 'firebase/app'
import 'firebase/firestore'
import { useUser } from '../../firebase/useUser'
const ReadDataFromCloudFirestore = () => {
const { user } = useUser()
const readData = () => {
try {
firebase
.firestore()
.collection('myCollection')
.doc(user.id)
.onSnapshot(function (doc) {
console.log(doc.data())
})
alert('Data was successfully fetched from cloud firestore! Close this alert and check console for output.')
} catch (error) {
console.log(error)
alert(error)
}
}
return (
<button onClick={readData}>Read Data From Cloud Firestore</button>
)
}
export default ReadDataFromCloudFirestore
import firebase from 'firebase/app'
import 'firebase/firestore'
import { useUser } from '../../firebase/useUser'
const WriteToCloudFirestore = () => {
const { user } = useUser()
const sendData = () => {
try {
firebase
.firestore()
.collection('myCollection')
.doc(user.id) // leave as .doc() for a random unique doc name to be assigned
.set({
string_data: 'Benjamin Carlson',
number_data: 2,
boolean_data: true,
map_data: { stringInMap: 'Hi', numberInMap: 7 },
array_data: ['text', 4],
null_data: null,
time_stamp: firebase.firestore.Timestamp.fromDate(new Date('December 17, 1995 03:24:00')),
geo_point: new firebase.firestore.GeoPoint(34.714322, -131.468435)
})
.then(alert('Data was successfully sent to cloud firestore!'))
} catch (error) {
console.log(error)
alert(error)
}
}
return (
<button onClick={sendData}>Send Data To Cloud Firestore</button>
)
}
export default WriteToCloudFirestore
That is it for Firebase authentication!
Realtime Database
Next, we'll add realtime database. We will be building a simmple button where when you press is, it increments a count. This count is stored in the database and is unique to each user. The count will then be fetched back and displayed to the user.
First, create a component named Counter.js
inside of the components/realtimeDatabase
folder. Create a basic component.
import { useState, useEffect } from 'react'
import firebase from 'firebase/app'
import 'firebase/database'
const Counter = ({ id }) => {
return (
<button onClick={increaseCount}>Increase count {count ? count : '–––'}</button>
)
}
export default Counter
As you can see, the button is called a method named increaseCount
. Let's create this. To do this, we will use the firebase.database().ref().child().off() syntax.
import { useState, useEffect } from 'react'
import firebase from 'firebase/app'
import 'firebase/database'
const Counter = ({ id }) => {
const [count, setCount] = useState('')
useEffect(() => {
const onCountIncrease = (count) => setCount(count.val())
const fetchData = async () => {
firebase.database().ref('counts').child(id).on('value', onCountIncrease)
}
fetchData()
return () => {
firebase.database().ref('counts').child(id).off('value', onCountIncrease)
}
}, [id])
const increaseCount = async () => {
const registerCount = () => fetch(`/api/incrementCount?id=${encodeURIComponent(id)}`)
registerCount()
}
return (
<button onClick={increaseCount}>Increase count {count ? count : '–––'}</button>
)
}
export default Counter
We ned to create the api route for this.
Create a file named fetchCount.js
inside of pages/api
.
Inside it we can fetch the count.
import firebase from 'firebase/app'
import 'firebase/database'
export default (req, res) => {
const ref = firebase.database().ref('counts').child(req.query.id)
return ref.once('value', (snapshot) => {
res.status(200).json({
total: snapshot.val()
})
})
}
Now, lets import the component inside index.js
and use it! Remember to pass in a unique id; I will be using the users uid from useUser
.
import Counter from '../components/realtimeDatabase/Counter'
// other imports
// other code above
<ReadDataFromCloudFirestore />
<Counter id={user.id} /> // added line
<button onClick={() => logout()}>Log Out</button>
// other code below
Storage
For storage, we only need to create one file. Inside of your components folder, create a folder named storage
and then
create a file named uploadFile.js
. First create a component with html input and progress.
import { useRef, useState } from 'react'
import firebase from 'firebase/app'
import 'firebase/storage'
const UploadFile = () => {
const inputEl = useRef(null)
const [value, setValue] = useState(0)
return (
<>
<progress value={value} max="100"></progress>
<input
type="file"
onChange={uploadFile}
ref={inputEl}
/>
</>
)
}
As you can see, we are using useState and useRef to handle the procress indicator and the file upload. To upload the
file, we need to get the file, and use .put
to send it to firebase.
function uploadFile() {
// get file
var file = inputEl.current.files[0]
// create a storage ref
var storageRef = firebase.storage().ref('user_uploads/' + file.name)
// upload file
var task = storageRef.put(file)
// update progress bar
task.on('state_change',
function progress(snapshot) {
setValue((snapshot.bytesTransferred / snapshot.totalBytes) * 100)
},
function error(err) {
alert(error)
},
function compleete() {
alert('Uploaded to firebase storage successfully!')
}
)
}
Finally, import this component into index.js
!
import ReadDataFromCloudFirestore from '../components/cloudFirestore/Read'
import { useUser } from '../firebase/useUser'
import Counter from '../components/realtimeDatabase/Counter'
import UploadFile from '../components/storage/UploadFile' // added line
export default function Home() {
const { user, logout } = useUser()
if (user) {
return (
<div>
<div className={styles.container}>
<h1>{user.name}</h1>
<h3>{user.email}</h3>
{user.profilePic ? <image src={user.profilePic} height={50} width={50}></image> : <p>No profile pic</p>}
<WriteToCloudFirestore />
<ReadDataFromCloudFirestore />
<Counter id={user.id} />
<UploadFile /> // added line
<button onClick={() => logout()}>Log Out</button>
</div>
// other code
)
Conclusion
With that, we have successfully added Firebase to our Next.js website.