Server Side Generation with Next.js support
3210jr opened this issue · 27 comments
Hey!
Is there anyway to support server side fetching of data so that the site can be populated and rendered on the server? This is for a Next.js SSG site for better SEO.
Thanks a lot!
PS: This library is a blessing!! Thanks for sharing it!
Hey, yes, there is. You’d have to use Next.js’s getInitialProps
, fetch the data there using the fuego.db
variable. Then pass the prop to the initialData
argument of useDocument
or useCollection
.
I can post an example if you need.
And I’m glad you like it! Always helpful if you tweet it out so it can help others too :) http://twitter.com/FernandoTheRojo
I haven't used this Next.js feature recently, but I think it would work something like this.
// pages/user/[id].tsx
import { useDocument, fuego } from '@nandorojo/swr-firestore'
import { NextPage } from 'next'
import { useRouter } from 'next/router'
type Props = {
initialData: object
}
const User: NextPage<Props> = ({ initialData }) => {
const { id } = useRouter().query
const { data } = useDocument<Props['initialData']>(`users/${id}`, { initialData })
return <div>{JSON.stringify(data)}</div>
}
User.getInitialProps = async ({ query }) => {
const { id } = query
const initialData = await fuego.db.doc(`users/${id}`).get()
return { initialData }
}
Ah, by the way, the example I put above is not for Server Side Generation (SSG). Instead, it's for Server Side Rendering. For anyone who doesn't know the difference – SSG generates your page based on static data when the app is built. On the other hand, SSR builds your screen from the server using dynamic data, every time the page is called.
If you want the same thing to work for SSG, you'd need to change getInitialProps
to getStaticProps
. Keep in mind, this only works for data that will be fetched when your app builds. If you want dynamic data, use the example above. I would expect dynamic to be more common with Firestore, but everyone has their own use cases!
// pages/home.tsx
import { useDocument, fuego } from '@nandorojo/swr-firestore'
import { NextPage, GetStaticProps } from 'next'
import { useRouter } from 'next/router'
type Props = {
initialData: object
}
const User = ({ initialData }: Props) => {
const { id } = useRouter().query
const { data } = useDocument<Props['initialData']>(`users/main`, { initialData })
return <div>{JSON.stringify(data)}</div>
}
export const getStaticProps: GetStaticProps = async () => {
const initialData = await fuego.db.doc(`users/main`).get()
return { initialData }
}
@nandorojo — Hi! I'm getting a "Cannot read property 'db' of undefined" when I try to use fuego
within getStaticProps
or getStaticPaths
even though Fuego
is initialized in _app.tsx
.
Has someone been able to make it work? It's very possible that I've made a mistake on my end. 😄
@danoc I think I know why. You're trying to access fuego
on the server side, but we're initializing it on the client side, since pages/_app.tsx
only runs on the client.
I think the solution would be to initialize fuego
in pages/_document.tsx
. I'm not sure if importing firebase like that will face any issues on the server, but could you try this and let me know if it fixes it?
import 'firebase/firestore'
import 'firebase/auth'
import { Fuego, setFuego } from '@nandorojo/swr-firestore'
setFuego(
new Fuego({
// ...your firebase config here
})
)
Here's a full example, copied from here.
// pages/_document.tsx
import Document, { Html, Head, Main, NextScript } from 'next/document'
import 'firebase/firestore'
import 'firebase/auth'
import { Fuego, setFuego } from '@nandorojo/swr-firestore'
setFuego(
new Fuego({
// ...your firebase config here
})
)
class MyDocument extends Document {
static async getInitialProps(ctx) {
const initialProps = await Document.getInitialProps(ctx)
return { ...initialProps }
}
render() {
return (
<Html>
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
}
export default MyDocument
If you have a minimal repo I can take a look at, I could also test it out there.
Just made a few changes to the example above.
Alternatively, you could import the actual fuego
variable you're making in pages/_app.tsx
. This variable is the same one imported from the library.
Closing this out since I think the solution(s) above should cover the use cases. Let me know if you face any problems, thanks!
Hey! First off, just wanted to say this looks like a really awesome project.
Since getStaticProps
and getServerSideProps
run in the Node.js context, I'm assuming you'd need to use Firebase Admin to communicate with Firestore. It looks like this library only uses Firebase client-side, so I'm guessing this won't work.
If you want to do SSG or SSR with Next.js, you'll likely need to also configure Firebase Admin in your project.
@leerob great catch, and @Lucas-Geitner thanks for the example! Glad we were able to resolve that.
Full tutorial for those lurking -> https://leerob.io/blog/nextjs-firebase-serverless
@Lucas-Geitner what is the custom swr you set on _app.js? I'm trying to get this work with Firebase admin as well.
Hello @creativityhurts,
I created a working repo you can fork, download, look and run : https://github.com/Lucas-Geitner/next-js-swr-firestore-exemple
@nandorojo We can put it as an exemple in the project if you would like to, such as what nextjs is doing
I'm open to adding an examples
folder soon.
I don't think we should initiating fuego
on the server side.
Something like this seems more appropriate:
import { Fuego, FuegoProvider } from "@nandorojo/swr-firestore";
import firebase from "firebase/app";
import "firebase/analytics"; // If you need it
import "firebase/auth"; // If you need it
import "firebase/firestore"; // If you need it
import "firebase/storage"; // If you need it
import React, { ReactElement, ReactNode, useEffect, useRef } from "react";
export type FirebaseCredentials = Parameters<
typeof firebase["initializeApp"]
>[0];
export const getFirebaseConfigFromEnv = (): FirebaseCredentials => ({
/* eslint-disable no-process-env */
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
databaseURL: process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL,
messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
/* eslint-enable no-process-env */
});
export type ManagedFuegoProviderProps = {
children?: ReactNode;
};
export const ManagedFuegoProvider = ({
children,
}: ManagedFuegoProviderProps): ReactElement => {
const fuego = useRef<Fuego | undefined>(undefined);
useEffect(() => {
const config = getFirebaseConfigFromEnv();
fuego.current = new Fuego(config);
}, []);
return <FuegoProvider fuego={fuego.current}>{children}</FuegoProvider>;
};
The problem is that FuegoProvider
expects an instance of Fuego
... and I'm not sure why. That instance will be a zero permission anonymous user ... and we're never even using that on the server side (again, using firebase-admin
).
@Lucas-Geitner I don't understand why you use the FuegoProvider in _app.tsx
and setFuego
in _document.tsx
... nor why you're initializing Fuego on the server side at all (again, you're not using it).
Here's how the NextJS example uses Firebase:
https://github.com/vercel/next.js/blob/f658b7641d5e500eca23b90ad3fa5705970f2525/examples/with-firebase/firebase/clientApp.js#L17-L22
...
Finally – thanks for thinking about all of this @nandorojo and @Lucas-Geitner. I'm excited to give this a shot... :)
From what I'm seeing, adding support for typeof firebase-admin.initializeApp()
right in the Fuego class should be straight forward (?).
The App returned in both cases can be treated as topologically equivalent for our use case I believe. See admin.app.App.firestore and firebase.app.App.firestore
Any thoughts?
@dfee In my experience, there are bugs trying to initialize fuego
in an effect. If you really want to do that, you can avoid the FuegoProvider
altogether and just call setFuego
in an effect. That's all the FuegoProvider
does under the hood. I don't recommend this, though.
I will post a simple way to use admin with fuego soon. It'll follow the same pattern as #39 (comment), where we create a custom ServerFuego
class and use it instead of Fuego
if we're on the server.
Waiting for this @nandorojo 👍 , please add some link to give u a coffee.
Please refer to #17 (comment) for now. @Lucas-Geitner you can remove fuego
from your _document.tsx
in that example, I believe.
I just removed _document.tsx and it's working like a charm.
I'm wondering if this solution supports the user's auth session server-side?
Hello @creativityhurts,
I created a working repo you can fork, download, look and run : https://github.com/Lucas-Geitner/next-js-swr-firestore-exemple
@nandorojo We can put it as an exemple in the project if you would like to, such as what nextjs is doing
This is awesome @Lucas-Geitner 🥇
Can you confirm my understanding of the below code:
function HomePage(props) {
const { data, update, error } = useCollection("users", {}, {initialData: props.usersData})
return <div>{JSON.stringify(data)}</div>
}
export const getStaticProps = async () => {
const users = await db.collection("users").get()
const usersData = users.docs.map(u => u.data())
return {props: {usersData}}
}
- You retrieve the data server-side using firebase-admin,
- You pass the data via the page props,
- You initialise the cache of useCollection for the same collection path (
users
)
Is it correct to say that only the server-side will perform actual reads on firestore, whereas useCollection at client-side will just return from the cache? I would like to avoid to read 2*n
documents when I have n
documents in the users
collection 😉
- You retrieve the data server-side using firebase-admin,
- You pass the data via the page props,
- You initialise the cache of useCollection for the same collection path (
users
)
If this is the way, then it also doesn't matter whether we're using Nextjs or another SSG solution. We grab the data on the server and pass it in as initial data on the client. Sounds solid... I'd also like to confirm that we won't be hitting firestore again when we initialise on the client
If your queries won't update when using initialData
, set revalidateOnMount
to true
in the SWR options flag.
Like so:
function HomePage(props) {
const { data, update, error } = useCollection("users",
// Ensure that `revalidateOnMount` is also set to `true`
{
initialData: props.usersData,
revalidateOnMount: true
}
)
return <div>{JSON.stringify(data)}</div>
}
export const getStaticProps = async () => {
const users = await db.collection("users").get()
const usersData = users.docs.map(u => u.data())
return {props: {usersData}}
}
See this issue: vercel/swr#284
Also related just linking to the same issue: #67
How can we handle auth session with this approach? Thanks