Search code examples
firebasegoogle-cloud-firestorenext.jscachingnext.js13

How to cache fetched data from firebase firestore in Next.js 13 app directory


I want to cache data on next js server side page, when we fetch data from firebase. i already know that we can cache default data using fetch api

https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating#fetching-data-on-the-server-with-fetch

but i want to cache data without this, as i don't have any APIs

basically in my next js app i have one dynamic route called "[username]"

enter image description here

So the example url will be "https://prozo.me/@hardikdesai", and i am fetching that user's data by username with this function

export const getUserDetailsByUsername = async (username: string): Promise<{ userDetails: any; message?: string }> => {
  const q = DB.collection('users').where('username', '==', username)
  const querySnapshot = await q.get()
  if (querySnapshot.empty) {
    return { userDetails: null, message: 'User not found' }
  }

  const userDetails = querySnapshot.docs[0].data()
  return { userDetails }
}

so, i want that my fetched data would ne cached for 60 seconds. and not make any firebase db request for that time

page.tsx

// sections
import { Metadata } from 'next'
import { getUserDetailsByUsername } from 'src/queries/user'
import { ProfileView } from 'src/sections/username/view'

// ----------------------------------------------------------------------
type Props = {
  params: { username: string }
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  return {
    title: `${decodeURIComponent(params?.username)} | Prozo`,
  }
}

export default async function ProfilePage({ params }: { params: { username: string } }) {
  const { userDetails } = await getUserDetailsByUsername(params?.username?.slice(3))
  return <ProfileView userDetails={userDetails} />
}

Solution

  • Next.js offers an experiential cache function designed to write to the Next.js Data Cache, functioning similarly to the extended fetch API.

    import { unstable_cache as cache } from "next/cache";
    
    const getUserDetailsByUsernameImpl = async (username: string): Promise<{ userDetails: any; message?: string }> => {
      const q = DB.collection('users').where('username', '==', username)
      const querySnapshot = await q.get()
      if (querySnapshot.empty) {
        return { userDetails: null, message: 'User not found' }
      }
    
      const userDetails = querySnapshot.docs[0].data()
      return { userDetails }
    }
    
    const getUserDetailsByUsername = cache(
      /* fetch function */ getUserDetailsByUsernameImpl,
      /* unique key     */ ["getUserDetailsByUsername"],
      /* options        */ {
        tags: ["getUserDetailsByUsername"],
        revalidate: 60 * 60 * 24 /* same as fetch.revalidate */
      }
    )
    

    Functioning similar to caching in the fetch API, it allows setting a revalidate option to define when cached data should become stale or invoking revalidateTag("getUserDetailsByUsername") to instantly invalidate the cache.

    It accepts 3 parameters:

    • A fetch function to be cached
    • An array of key parts ensuring global uniqueness when combined.
    • An options object containing:
      • tags: An array of keys usable with revalidateTag.
      • revalidate: The interval in seconds after which the cache should be revalidated.

    Read more in the documentation: https://nextjs.org/docs/app/api-reference/functions/unstable_cache


    Advanced:

    Currently, there's a limitation with revalidateTag where individual user revalidation isn't supported, for instance, revalidateTag("getUserDetailsByUsername", "JohnDoe") isn't feasible. Invoking revalidateTag("getUserDetailsByUsername") invalidates all cached users. A workaround involves a slight restructuring:

    const getUserDetailsByUsername = async (username) => {
      const cachedFn = cache(
        getUserDetailsByUsernameImpl,
        [`getUserDetailsByUsername-${username}`],
        {
          tags: ["getUserDetailsByUsername", `getUserDetailsByUsername-${username}`],
          revalidate: 60 * 60 * 24
        }
      )
    
      return cachedFn(username); 
    }
    

    This approach includes the username in the cache tag, enabling the revalidation of a specific user when necessary:

    revalidateTag(`getUserDetailsByUsername-${username}`)