Handling caching issues with Sanity data in NextJS app router

4 replies
Last updated: May 24, 2024
Using NextJS app router
next@14.2.3

When I load data from sanity it seems that it gets cached. the only way to avoid caching is to provide to the client.fetch an extra parameter. but then typescript doesn’t recognise this… Though it works and there is no cache..


import groq from 'groq';

import { Footer } from '@/components/footer';
import { Header } from '@/components/header';
import { type ContentItem, renderComponent } from '@/lib/render';
import { client } from '@/sanity/lib/client';

type DataProps = {
  content: ContentItem[];
};

const pageQuery = groq`*[_type == "page" && slug.current == $slug]`;

async function getData(slug: string) {
  const res = await client.fetch(pageQuery, { slug, next: { revalidate: 1 } });
  if (!res) {
    throw new Error('Failed to fetch data');
  }

  return res[0];
}

export default async function Page() {
  const slug = '/';
  const data: DataProps = await getData(slug);
  console.log('page.tsx data', data);

  return (
    <main className="flex flex-col items-center">
      <Header />
      {data?.content.map(renderComponent)}
      <Footer />
    </main>
  );
}
AI Update

I can see what's happening here! You're experiencing Next.js App Router caching, and while your workaround works at runtime, TypeScript doesn't recognize the cache options. Let me clarify what's actually going on and show you the proper solutions.

The Real Issue

Your code is actually working correctly at runtime - passing { slug, next: { revalidate: 1 } } as the second parameter does work. The TypeScript error occurs because the Sanity client types don't explicitly define support for Next.js-specific cache options in that parameter, even though Next.js's fetch patching makes it work under the hood.

However, mixing query parameters with cache options in the same object isn't the cleanest approach. Here are better solutions:

The most idiomatic Next.js App Router approach is to use route segment configuration at the page level instead of passing cache options to individual fetch calls:

// page.tsx
export const revalidate = 1; // Revalidate every 1 second
// or
export const dynamic = 'force-dynamic'; // Always fetch fresh data

const pageQuery = groq`*[_type == "page" && slug.current == $slug]`;

async function getData(slug: string) {
  const res = await client.fetch(pageQuery, { slug }); // Clean, no cache options needed
  if (!res) {
    throw new Error('Failed to fetch data');
  }
  return res[0];
}

export default async function Page() {
  const slug = '/';
  const data: DataProps = await getData(slug);
  
  return (
    <main className="flex flex-col items-center">
      <Header />
      {data?.content.map(renderComponent)}
      <Footer />
    </main>
  );
}

This is cleaner, avoids TypeScript issues entirely, and applies caching behavior to the entire route segment.

Solution 2: Type Assertion (Quick Fix)

If you need per-fetch control and want to keep your current approach, use a type assertion to satisfy TypeScript:

async function getData(slug: string) {
  const res = await client.fetch(
    pageQuery, 
    { slug, next: { revalidate: 1 } } as { slug: string }
  );
  
  if (!res) {
    throw new Error('Failed to fetch data');
  }
  return res[0];
}

Or for no caching at all:

const res = await client.fetch(
  pageQuery, 
  { slug, cache: 'no-store' } as { slug: string }
);

Solution 3: Configure Your Sanity Client

You can also configure your Sanity client to disable CDN caching by default when creating it:

// sanity/lib/client.ts
import { createClient } from 'next-sanity';

export const client = createClient({
  projectId: 'your-project-id',
  dataset: 'production',
  apiVersion: '2024-01-01',
  useCdn: false, // Disable CDN caching for always-fresh content
});

Note that useCdn: false fetches directly from the Sanity API rather than the CDN, giving you fresh data but with slightly higher latency.

Understanding Next.js 14 Caching

By default, Next.js 14 App Router aggressively caches all fetch requests. Your options for controlling this:

  • revalidate: number - Time-based revalidation (ISR)
  • cache: 'no-store' - Never cache (always fresh)
  • cache: 'force-cache' - Always cache (default behavior)
  • Route segment config - Apply caching rules to entire routes

Why Route Segment Config is Best

Using export const revalidate = 1 or export const dynamic = 'force-dynamic' at the page level is the recommended pattern because:

  1. No TypeScript issues - You're using Next.js features as intended
  2. Cleaner code - Cache configuration is separate from data fetching logic
  3. Consistent behavior - All fetches in that route follow the same caching rules
  4. Better DX - More explicit and easier to understand what's being cached

For most content-driven sites, setting export const revalidate = 60 (or whatever interval makes sense) at the page level gives you a great balance of performance and freshness without complicating your fetch calls.

how do I get typescript to recognise this prop (which I think is just a
fetch
and nextjs is decorating the
fetch
with extra props
actually I’m not sure this actually solves the cache…
😞
okay, this is the solution:

async function getData(slug: string) {
  const res = await client.fetch(pageQuery, { slug }, { cache: 'no-store' });
  if (!res) {
    throw new Error('Failed to fetch data');
  }

  return res[0];
}
source:
https://www.sanity.io/answers/how-to-disable-cache-in-sanity-io-for-immediate-reflection-of-changes-made-in-the-studio-on-the-blog-

Sanity – Build the way you think, not the way your CMS thinks

Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.

Was this answer helpful?