nandorojo/swr-firestore

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 }
}
danoc commented

@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.

Yes it's totally working with the firebase admin, this is an exemple :

pages/index.js

code

lib/firebase.js

Capture d’écran 2020-07-13 à 13 49 19

And i set a custom swr on _app.js.

@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.

dfee commented

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}}
}
  1. You retrieve the data server-side using firebase-admin,
  2. You pass the data via the page props,
  3. 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