Realtime SSR with Nuxt3 and Firebase
Nuxt3 hit release candidate recently with a variety of awesome new features. But you might be wondering… how do I use Nuxt3 with Firebase? There is no documented best practice as of today, so I put together a demo that is capable of server-side rendering (SSR) via Firestore, followed by hydration to secure realtime data on the client. This gives a website the benefits SEO-friendly HTML, without sacrificing the power the realtime data updates after the initial page load.
Initial Setup
To get started, you will need a Firebase project then generate a new Nuxt app.
npx nuxi init nuxt-app
cd nuxt-app
npm install firebase firebase-admin
Server-Side Rendering
The Firebase Admin SDK is the preferred way to run Firebase server-side. In your Nuxt project, create the following files:
/server/api/animal.js
/server/utils/firebase.js
Initialize Firebase Admin
Firebase Admin requires us to authenticate the server. Download your service account from the Firebase console and save it as service-account.json
in the root of the project. ⚠ Keep this file secret!
Now install the Firebase Admin SDK and export Firestore.
import { initializeApp, cert } from 'firebase-admin/app';
import { getFirestore } from 'firebase-admin/firestore';
export const app = initializeApp({
credential: cert('./service-account.json'),
})
export const firestore = getFirestore();
Firestore API Route
Create an API route to fetch data from Firestore and make it accessible over HTTP.
import { firestore } from '../utils/firebase';
export default defineEventHandler(async event => {
const ref = firestore.doc(`animals/dog`);
const snapshot = await ref.get();
const data = snapshot.data();
return data;
})
Fetch Data via Page
Now create a Vue component or page to fetch the data and display it in the UI.
<script setup>
// Server Side
const { data: serverData } = useFetch('/api/animal');
</script>
<template>
<div>
<h2>Server</h2>
<pre>{{ serverData }}</pre>
</div>
</template>
Client-Side Hydration
At this point, we are able to fetch data from the server, but it will not respond to realtime updates. To add a realtime listener, we need to initialize the Firebase Web SDK and hydrate the data.
Note: This process results in 2 document reads on the initial page load.
Initialize the Firebase Web SDK
Grab the Firebase web config, then create a composable to access Firebase from a Vue component.
import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";
export const useFirebase = () => {
const firebaseConfig = {
// your config
};
const firebaseApp = initializeApp(firebaseConfig);
const firestore = getFirestore(firebaseApp);
return {
firebaseApp,
firestore,
}
}
Hydrate Realtime Data
When the client loads, the app needs to refetch the same data and listen to changes with onSnapshot
. It’s important to only run Firebase Web code on the client by placing it inside the onMounted
lifecycle hook on inside a function.
<script setup>
import { doc, onSnapshot, getDoc, updateDoc } from "firebase/firestore";
// Server Side
const { data } = useFetch('/api/animal');
// Client Side
onMounted(async() => {
const { firestore } = useFirebase();
const docRef = doc(firestore, `animals`, 'dog');
onSnapshot(docRef, (snap) => {
data.value = snap.data();
});
});
const updateAnimal = async() => {
const { firestore } = useFirebase();
const docRef = doc(firestore, `animals`, 'dog');
await updateDoc(docRef, {
name: `dog-${Math.floor(Math.random() * 1000)}`,
});
}
</script>
<template>
<div>
<h2>Data</h2>
<pre>{{ data }}</pre>
<button @click="updateAnimal">Update dog</button>
</div>
</template>