falcondev-oss / trpc-vue-query

Fully type-safe composables and helpers to make working with tRPC + Tanstack Query as intuitive as possible.
https://trpc-vue-query.falcondev.io
MIT License
17 stars 1 forks source link

How to handle SWR cache in useQuery() to avoid fetching if the SWR cache exists? #5

Closed dimasxp closed 5 months ago

dimasxp commented 5 months ago

How to handle SWR cache in useQuery() to avoid fetching if the SWR cache exists?

[trpc].ts

export default createNuxtApiHandler({
    router: appRouter,
    createContext,
    responseMeta(opts) {
        const ONE_DAY_IN_SECONDS = 60 * 60 * 24;
        return {
            headers: {
                "cache-control":  `s-maxage=300, stale-while-revalidate=${ONE_DAY_IN_SECONDS}`,
            },
        };
    },
});

projects.vue page

const { data, isPending, suspense } = useTrpc().project.getProjects.useQuery(() => {
    return {
      category: route_category,
      pageSize: pageSize.value,
      page: currentPage.value,
    }
  },
    {
      refetchOnMount: false,
      refetchOnReconnect: false,
      refetchOnWindowFocus: false,
      staleTime: 1000 * 60 * 5,
    },
  )

  onServerPrefetch(async () => {
    await suspense()
  })

At the moment, each time the project page loads, it refetches

LouisHaftmann commented 5 months ago

Have you tried hard coding the parameters (category, pageSize and page) for testing?

Could you also provide a reproduction?

dimasxp commented 5 months ago

Here is the reproduction. https://stackblitz.com/edit/nuxt-starter-u6ofn7?file=pages%2F%5B...page%5D.vue

I want to achieve that page 1 and page 2 are cached using SWR, so they do not trigger a load or a promise each time the page is reloaded or when switching between pages

if i right understood i cant use direct the nuxt routeRules for this

Thank you for any help 🙏

LouisHaftmann commented 5 months ago

Sorry, I misunderstood. I thought that the caching of vue-query wasn't working. If you want to persist the api response data between page loads, there are numerous options:

Sadly you cannot use Nitro's built-in caching behaviour with tRPC because Nitro handles caching on a per-route basis and tRPC only uses one route (/api/trpc/[trpc])

LouisHaftmann commented 5 months ago

Here's a pseudo caching plugin that would allow you to achieve similar behaviour as Cloudflare or Vercel:

export default defineNitroPlugin(nitro => {
  nitro.hooks.hook('request', async (event) => {
    // construct key from request

    // look for key in cache

    // if found, send cached response and return

    // if not found, continue
  })
  nitro.hooks.hook('afterResponse', (event) => {
    // construct key from request

    // cache response according to cache headers if not already cached

    // return response
  })
})
dimasxp commented 5 months ago

Thank you, and sorry for my unclear explanation. Yes, I plan to deploy to Vercel and use this approach:

Just set cache headers on your API responses and use a cache proxy like Cloudflare or Vercel.

For some reason, this is not working for me. I tried to set the response headers here. https://stackblitz.com/edit/nuxt-starter-u6ofn7?file=server%2Fapi%2Ftrpc%2F%5Btrpc%5D.ts Did I miss something?

LouisHaftmann commented 5 months ago

Could you share the response cache headers from your vercel preview deployment? Also take a look at this: https://vercel.com/docs/edge-network/caching#cacheable-response-criteria.

You might have other headers that prevent it from being cached (like Set-Cookie)

dimasxp commented 5 months ago

Oh, yes, thanks for the link

here the response CleanShot 2024-06-06 at 3  27 06@2x

Apparently this is due to cookies? I'm trying to figure out how to remove them for certain trpc paths

LouisHaftmann commented 5 months ago

Something like this:


export function createContext(event: H3Event) {
  return {
    event,
  }
}

export type Context = inferAsyncReturnType<typeof createContext>

export interface CacheOptions {
  /**
   * Max age in seconds
   */
  maxAge: number
  /**
   * Stale while revalidate in seconds
   */
  swr: number
}
const t = initTRPC
  .context<Context>()
  .meta<
    Partial<{
      cache: CacheOptions
    }>
  >()
  .create()

export const publicProcedure = procedure.use(async ({ ctx, meta, next }) => {
  if (meta?.cache) {
    // don't cache in browser
    setHeader(ctx.event, 'cache-control', 'no-cache')
    // cache in CDN
    setHeader(
      ctx.event,
      `cdn-cache-control`,
      `public, max-age=${meta.cache.maxAge}, stale-while-revalidate=${meta.cache.swr}`,
    )
  } else {
    setHeader(ctx.event, 'cache-control', 'no-store')
  }

  return next()
})

router({
  hello: publicProcedure
    .meta({
      cache: {
        maxAge: 60 * 60 * 24,
        swr: 60 * 60 * 24 * 30,
      },
    })
    .query(async ({ ctx }) => {
      return 'Hello world'
    }),
})
dimasxp commented 5 months ago

handy solution, thanks. implemented and cache now seems working in browser, loads fast but on the server side in vercel logs i see that on each page reload its still trigger long time promise. CleanShot 2024-06-06 at 3  30 52

CleanShot 2024-06-06 at 3  52 30

CleanShot 2024-06-06 at 3  31 51@2x

LouisHaftmann commented 5 months ago

This is probably because during SSR Nuxt directly calls the api route handlers without doing a full http request (https://nuxt.com/docs/api/utils/dollarfetch). Since it doesn't go through Vercel's caching layer, it doesn't get cached. Only fix for this would probably be to cache the whole page.

dimasxp commented 5 months ago

Solved. I used all your suggestions. Thanks for your help and time.