Handling caching issues with Sanity data in NextJS app router
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:
Solution 1: Route Segment Config (Recommended)
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:
- No TypeScript issues - You're using Next.js features as intended
- Cleaner code - Cache configuration is separate from data fetching logic
- Consistent behavior - All fetches in that route follow the same caching rules
- 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.
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.