Datahämtning och cachning i Next.js App Router: Komplett guide med use cache, ISR och Server Components

Komplett guide till datahämtning och cachning i Next.js App Router. Lär dig use cache-direktivet, ISR, revalidateTag, parallell datahämtning och hur du kombinerar cachningslager för optimala prestanda.

Datahämtning och cachning i Next.js App Router: Allt du behöver veta

Okej, vi måste prata om datahämtning i Next.js. Det har förändrats rejält de senaste åren. Om du är van vid getServerSideProps och getStaticProps från Pages Router kan det nya systemet i App Router kännas överväldigande — men ärligt talat, när du väl förstår det inser du att det egentligen är enklare och mer intuitivt. Problemet har mest varit att cachningsmodellen ändrats mellan versioner, och att dokumentationen inte alltid hunnit med.

Jag har själv suttit och slitit mitt hår över cachningsbeteenden som verkade helt oförklarliga, bara för att upptäcka att jag blandat ihop Next.js 14-logik med Next.js 15-defaults. Det händer fler än du tror.

I den här guiden går vi igenom hela ekosystemet för datahämtning och cachning i Next.js App Router. Från grundläggande async/await i Server Components till det helt nya use cache-direktivet i Next.js 16, via ISR, on-demand revalidering och parallell datahämtning. Allt med fungerande kodexempel som du kan köra direkt.

Om du redan läst vår guide om Server Actions vet du hur mutationer fungerar. Den här artikeln kompletterar den genom att fokusera på läsoperationer — hur du hämtar, cachar och revaliderar data på ett optimalt sätt.

Grunderna: Datahämtning i Server Components

React Server Components (RSC) är standardläget i App Router. Dina komponenter körs alltså på servern som standard, och du kan hämta data direkt med async/await — utan useEffect, useState eller externa bibliotek. Ja, det är verkligen så enkelt.

Direkt datahämtning med fetch

Det enklaste sättet att hämta data i en Server Component är att använda webbens inbyggda fetch-API:

// app/articles/page.tsx
export default async function ArticlesPage() {
  const response = await fetch('https://api.example.com/articles');
  const articles = await response.json();

  return (
    <main>
      <h1>Artiklar</h1>
      <ul>
        {articles.map((article: Article) => (
          <li key={article.id}>{article.title}</li>
        ))}
      </ul>
    </main>
  );
}

Några viktiga saker att notera:

  • Komponenten är async — den kan använda await direkt
  • Ingen klient-JavaScript skickas till webbläsaren för datahämtningen
  • API-nycklar och känslig data stannar säkert på servern
  • Du kan göra databasanrop direkt — ingen separat API-route behövs

Databasanrop direkt i komponenter

Eftersom Server Components körs på servern kan du använda ORM:er och databasklienter direkt. Ingen mellanliggande API-route krävs, och det här är ärligt talat en av de saker som fick mig att verkligen uppskatta App Router:

// app/products/page.tsx
import { db } from '@/lib/db';

export default async function ProductsPage() {
  const products = await db.product.findMany({
    where: { published: true },
    orderBy: { createdAt: 'desc' },
    take: 20,
  });

  return (
    <main>
      <h1>Produkter</h1>
      {products.map((product) => (
        <article key={product.id}>
          <h2>{product.name}</h2>
          <p>{product.price} kr</p>
        </article>
      ))}
    </main>
  );
}

Du eliminerar hela API-lagret för interna dataoperationer. Det förenklar kodbasen avsevärt — färre filer, färre abstraktioner, mindre att underhålla.

Cachningens fyra lager i Next.js

Nu börjar det bli intressant (och lite krångligt). Cachningen i Next.js App Router består av fyra distinkta lager som samverkar. Att förstå dem är avgörande för att bygga snabba applikationer — och för att slippa de frustrationer som garanterat uppstår när cachningen inte beter sig som du förväntar dig.

1. Request Memoization (servern, per renderingscykel)

React deduplicerar automatiskt identiska fetch-anrop inom samma renderingscykel. Så om du råkar hämta samma data i en layout och en sida görs bara ett faktiskt nätverksanrop:

// app/layout.tsx
export default async function Layout({ children }) {
  // Denna fetch...
  const user = await fetch('https://api.example.com/user');
  return <div>{children}</div>;
}

// app/page.tsx
export default async function Page() {
  // ...och denna fetch görs bara EN gång totalt
  const user = await fetch('https://api.example.com/user');
  return <div>Hej {user.name}</div>;
}

Det här gäller bara för GET-anrop med samma URL och alternativ. Cachen rensas helt efter varje serverrendering, så det är egentligen bara en optimering inom en enda request.

2. Data Cache (servern, persistent)

Data Cache lagrar fetch-resultat mellan förfrågningar och byggen. En viktig sak: i Next.js 15+ är fetch-anrop inte cachade som standard. Du måste explicit välja cachning:

// Inte cachad (standard i Next.js 15+)
const data = await fetch('https://api.example.com/data');

// Cachad explicit
const cachedData = await fetch('https://api.example.com/data', {
  cache: 'force-cache',
});

// Cachad med tidsbaserad revalidering
const revalidatedData = await fetch('https://api.example.com/data', {
  next: { revalidate: 3600 }, // Revalidera efter 1 timme
});

Obs: Denna ändring från Next.js 14 (där allt cachades automatiskt) till Next.js 15+ (inget cachas som standard) är en av de absolut vanligaste källorna till förvirring. Om du uppgraderar ett äldre projekt — var beredd på att explicit aktivera cachning där du behöver den. Det här har bitit många utvecklare, inklusive mig själv.

3. Full Route Cache (servern, vid build)

Statiska routes förrenderas vid byggtid. HTML och React Server Component Payload (RSC Payload) lagras och serveras direkt utan extra bearbetning. Dynamiska routes renderas däremot vid varje förfrågan.

En route blir dynamisk om den använder dynamiska API:er som cookies(), headers() eller searchParams, eller om den har en ocachad fetch-förfrågan. Det är bra att ha koll på, annars kan du få oväntade prestandaproblem.

4. Router Cache (klienten, i minnet)

Webbläsaren cachar RSC-payloads för besökta routes. Varaktigheten beror på routetyp:

  • Statiska routes: 5 minuter
  • Dynamiska routes: 30 sekunder

Du kan ogiltigförklara denna cache med router.refresh() i en Client Component. Det är användbart när du vet att data just ändrats och vill tvinga en uppdatering.

use cache — det nya sättet att cacha i Next.js 16

Så, här kommer den stora nyheten. Next.js 16 introducerade use cache-direktivet — ett helt nytt och mer explicit sätt att hantera cachning. Istället för att cachning sker automatiskt (Next.js 14-stilen) eller att du måste pilla med fetch-specifika alternativ, markerar du nu explicit vilka sidor, komponenter eller funktioner som ska cachas.

Jag tycker personligen att det här är en av de bästa förändringarna i Next.js på länge. Det gör cachning mycket mer förutsägbart.

Aktivera Cache Components

Först behöver du aktivera funktionen i next.config.ts:

// next.config.ts
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  cacheComponents: true,
};

export default nextConfig;

Cacha en hel sida

Det enklaste användningsfallet — markera en hel sida som cachad:

// app/blog/page.tsx
import { cacheLife } from 'next/cache';

export default async function BlogPage() {
  'use cache';
  cacheLife('hours');

  const posts = await db.post.findMany({
    where: { published: true },
    orderBy: { createdAt: 'desc' },
  });

  return (
    <main>
      <h1>Bloggen</h1>
      {posts.map((post) => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </main>
  );
}

Med 'use cache' högst upp i komponenten cachar Next.js hela sidans output. cacheLife('hours') anger att cachen ska gälla i timmar. Du kan också använda 'days', 'weeks', 'max' eller definiera helt egna profiler (mer om det strax).

Cacha enskilda funktioner

Du behöver inte cacha hela sidan. Ofta vill du bara cacha specifika datahämtningsfunktioner:

// lib/data.ts
import { cacheLife, cacheTag } from 'next/cache';

export async function getProducts(categoryId: string) {
  'use cache';
  cacheLife('hours');
  cacheTag('products', `category-${categoryId}`);

  const products = await db.product.findMany({
    where: { categoryId, published: true },
    include: { images: true },
  });

  return products;
}

export async function getProductById(id: string) {
  'use cache';
  cacheLife('days');
  cacheTag(`product-${id}`);

  const product = await db.product.findUnique({
    where: { id },
    include: { images: true, reviews: true },
  });

  return product;
}

Notera hur cacheTag låter dig tagga cache-poster för on-demand-ogiltigförklaring — vi återkommer till det i sektionen om revalidering.

Definiera egna cache-profiler

De inbyggda profilerna räcker inte alltid. Ibland vill du ha mer kontroll, och då kan du definiera egna i konfigurationen:

// next.config.ts
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  cacheComponents: true,
  cacheLife: {
    blog: {
      stale: 3600,       // 1 timme — servera stale i denna period
      revalidate: 900,   // 15 minuter — revalidera i bakgrunden
      expire: 86400,     // 1 dag — max livslängd
    },
    productListing: {
      stale: 600,        // 10 minuter
      revalidate: 300,   // 5 minuter
      expire: 3600,      // 1 timme
    },
  },
};

export default nextConfig;

Sedan använder du dem så här:

export async function getBlogPosts() {
  'use cache';
  cacheLife('blog');
  // ...
}

Snyggt och enkelt. Du bestämmer exakt hur länge olika typer av data ska cachas.

Partial Prerendering (PPR)

Cache Components kompletterar Partial Prerendering — möjligheten att blanda statiskt och dynamiskt innehåll i samma route. Statiska delar (markerade med use cache) serveras direkt som HTML, medan dynamiska delar streamar in via Suspense:

// app/dashboard/page.tsx
import { Suspense } from 'react';
import { cacheLife } from 'next/cache';

// Denna komponent cachas
async function SiteStats() {
  'use cache';
  cacheLife('hours');

  const stats = await db.stats.aggregate();
  return (
    <section>
      <h2>Statistik</h2>
      <p>Totalt {stats.totalUsers} användare</p>
    </section>
  );
}

// Denna komponent är dynamisk (ej cachad)
async function RecentActivity() {
  const activities = await db.activity.findMany({
    orderBy: { createdAt: 'desc' },
    take: 10,
  });
  return (
    <section>
      <h2>Senaste aktiviteten</h2>
      {activities.map((a) => (
        <p key={a.id}>{a.description}</p>
      ))}
    </section>
  );
}

export default function DashboardPage() {
  return (
    <main>
      <h1>Dashboard</h1>
      <SiteStats />
      <Suspense fallback={<p>Laddar aktivitet...</p>}>
        <RecentActivity />
      </Suspense>
    </main>
  );
}

Här serveras SiteStats direkt från cachen medan RecentActivity renderas dynamiskt. Användaren ser den statiska delen omedelbart, och den dynamiska streamas in. Ärligt talat — det här är en genialisk kombination av prestanda och fräschör.

ISR — Incremental Static Regeneration

ISR ger dig det bästa av två världar: statisk prestanda med möjlighet att uppdatera enskilda sidor utan att bygga om hela sajten. I App Router konfigurerar du ISR via route-segmentkonfiguration eller per fetch-anrop.

Tidsbaserad revalidering

Det enklaste sättet att implementera ISR:

// app/blog/[slug]/page.tsx
export const revalidate = 3600; // Revalidera var 60:e minut

export async function generateStaticParams() {
  const posts = await db.post.findMany({ select: { slug: true } });
  return posts.map((post) => ({ slug: post.slug }));
}

export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const post = await db.post.findUnique({ where: { slug } });

  if (!post) notFound();

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

Med revalidate = 3600 serveras den cachade sidan i 60 minuter. Nästa förfrågan efter det triggar en bakgrundsregenerering — den aktuella besökaren får fortfarande den cachade versionen, men nästa besökare får den uppdaterade. Smidigt.

Per-fetch revalidering

Du kan också styra revalidering per enskild fetch-förfrågan, vilket ger dig finare kontroll:

// Olika revalideringstider för olika data
const globalConfig = await fetch('https://api.example.com/config', {
  next: { revalidate: 86400 }, // Revalidera dagligen
});

const latestPosts = await fetch('https://api.example.com/posts', {
  next: { revalidate: 600 }, // Revalidera var 10:e minut
});

const liveComments = await fetch('https://api.example.com/comments', {
  cache: 'no-store', // Aldrig cacha
});

On-demand revalidering: revalidateTag och revalidatePath

Tidsbaserad revalidering är bra, men den räcker inte alltid. Ibland behöver du ogiltigförklara cachad data direkt — till exempel när en användare uppdaterar en bloggpost eller när ditt CMS skickar en webhook. Att vänta 60 minuter på att en ändring syns är liksom inte optimalt.

revalidateTag — granulär ogiltigförklaring

revalidateTag låter dig ogiltigförklara alla cache-poster med en specifik tagg, oavsett var de används. Det här är det rekommenderade sättet för finkorning cachekontroll:

// app/actions.ts
'use server';

import { revalidateTag } from 'next/cache';

export async function updateArticle(id: string, formData: FormData) {
  const title = formData.get('title') as string;
  const content = formData.get('content') as string;

  await db.article.update({
    where: { id },
    data: { title, content },
  });

  // Ogiltigförklara cachade data med dessa taggar
  revalidateTag(`article-${id}`);
  revalidateTag('articles-list');
}

Och i din datahämtningsfunktion taggar du dina fetch-anrop:

// lib/data.ts
export async function getArticles() {
  const response = await fetch('https://api.example.com/articles', {
    next: { tags: ['articles-list'] },
  });
  return response.json();
}

export async function getArticle(id: string) {
  const response = await fetch(`https://api.example.com/articles/${id}`, {
    next: { tags: [`article-${id}`, 'articles-list'] },
  });
  return response.json();
}

Med use cache och cacheTag kan du även tagga icke-fetch-operationer som databasanrop. Det är faktiskt en av de finaste sakerna med det nya systemet:

// lib/data.ts
import { cacheTag } from 'next/cache';

export async function getArticlesFromDb() {
  'use cache';
  cacheTag('articles-list');

  return db.article.findMany({
    where: { published: true },
    orderBy: { createdAt: 'desc' },
  });
}

revalidatePath — ogiltigförklara hela routes

revalidatePath ogiltigförklarar Data Cache och Full Route Cache för en specifik sökväg. Lite mindre kirurgiskt, men ibland är det precis vad du behöver:

// app/actions.ts
'use server';

import { revalidatePath } from 'next/cache';

export async function publishArticle(id: string) {
  await db.article.update({
    where: { id },
    data: { published: true },
  });

  // Ogiltigförklara hela bloggsidan
  revalidatePath('/blog');

  // Ogiltigförklara den specifika artikelsidan
  revalidatePath(`/blog/${id}`);
}

Webhook-baserad revalidering

Om du använder ett headless CMS kan du trigga revalidering via en Route Handler som tar emot webhooks. Det här är ett vanligt mönster i produktionsmiljöer:

// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache';
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
  const secret = request.headers.get('x-revalidation-secret');

  if (secret !== process.env.REVALIDATION_SECRET) {
    return NextResponse.json({ error: 'Ogiltigt secret' }, { status: 401 });
  }

  const body = await request.json();
  const { tag } = body;

  if (!tag || typeof tag !== 'string') {
    return NextResponse.json(
      { error: 'Tagg krävs' },
      { status: 400 }
    );
  }

  revalidateTag(tag);

  return NextResponse.json({
    revalidated: true,
    tag,
    timestamp: Date.now(),
  });
}

revalidateTag vs revalidatePath — när använder du vad?

Det korta svaret: det beror på.

  • revalidateTag: Använd när samma data visas på flera sidor. Ogiltigförklarar alla cache-poster med den taggen, oavsett vilken route de hör till. Perfekt för delade data som kategorier, menyalternativ eller globala inställningar.
  • revalidatePath: Använd när du vill ogiltigförklara en specifik sida eller layout. Enklare men mindre granulär — hela sidans cache ogiltigförklaras, inklusive all data på den sidan.

I praktiken kombinerar du ofta båda: revalidateTag för specifika datapunkter och revalidatePath för sidorna som visar dem. Det ger bäst kontroll.

Parallell och sekventiell datahämtning

Hur du strukturerar dina datahämtningar påverkar prestandan enormt. Next.js hanterar layouts och sidor parallellt automatiskt, men inom en komponent kan sekventiella anrop skapa onödigt långa väntetider. Det här är ett av de enklaste sätten att snabba upp din app.

Problemet med sekventiell datahämtning

// DÅLIGT — sekventiell datahämtning (waterfall)
export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;

  // Vänta tills produkt laddats...
  const product = await getProduct(id);

  // ...innan recensioner börjar laddas
  const reviews = await getReviews(id);

  // ...och sedan relaterade produkter
  const related = await getRelatedProducts(product.categoryId);

  return <div>{/* ... */}</div>;
}

Varje anrop väntar på att det föregående ska slutföras. Om varje tar 200ms blir den totala tiden 600ms. Det kanske inte låter mycket, men det märks — speciellt på mobil.

Parallell datahämtning med Promise.all

// BRA — parallell datahämtning
export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;

  // Starta alla anrop samtidigt
  const [product, reviews] = await Promise.all([
    getProduct(id),
    getReviews(id),
  ]);

  // Relaterade produkter beror på product.categoryId
  const related = await getRelatedProducts(product.categoryId);

  return <div>{/* ... */}</div>;
}

Nu körs getProduct och getReviews samtidigt, vilket halverar väntetiden. Enkelt, men det gör stor skillnad.

Streaming med Suspense

Ännu bättre — du kan låta snabba komponenter visas direkt medan långsammare data laddar i bakgrunden:

// app/products/[id]/page.tsx
import { Suspense } from 'react';

async function ProductDetails({ id }: { id: string }) {
  const product = await getProduct(id);
  return (
    <section>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <p className="price">{product.price} kr</p>
    </section>
  );
}

async function ProductReviews({ id }: { id: string }) {
  const reviews = await getReviews(id);
  return (
    <section>
      <h2>Recensioner ({reviews.length})</h2>
      {reviews.map((review) => (
        <div key={review.id}>
          <p>{review.comment}</p>
          <span>{review.rating}/5</span>
        </div>
      ))}
    </section>
  );
}

export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;

  return (
    <main>
      <Suspense fallback={<p>Laddar produkt...</p>}>
        <ProductDetails id={id} />
      </Suspense>

      <Suspense fallback={<p>Laddar recensioner...</p>}>
        <ProductReviews id={id} />
      </Suspense>
    </main>
  );
}

Med Suspense visas varje del av sidan så snart dess data är klar. Produktinfo kan dyka upp direkt medan recensionerna fortfarande laddar. Användarupplevelsen blir markant bättre.

Skicka data från server till klient

Ibland behöver du interaktivitet i webbläsaren men vill ändå hämta data på servern. Reacts use-API (nytt i React 19) gör detta möjligt genom att skicka Promises som props.

Streama data till Client Components

// app/dashboard/page.tsx (Server Component)
import { Suspense } from 'react';
import { DashboardChart } from './DashboardChart';

async function getChartData() {
  const data = await db.analytics.getMonthlyData();
  return data;
}

export default function DashboardPage() {
  const chartDataPromise = getChartData();

  return (
    <main>
      <h1>Dashboard</h1>
      <Suspense fallback={<p>Laddar diagram...</p>}>
        <DashboardChart dataPromise={chartDataPromise} />
      </Suspense>
    </main>
  );
}

// app/dashboard/DashboardChart.tsx (Client Component)
'use client';

import { use } from 'react';

interface DashboardChartProps {
  dataPromise: Promise<ChartData[]>;
}

export function DashboardChart({ dataPromise }: DashboardChartProps) {
  const data = use(dataPromise);

  return (
    <div>
      {/* Rendera interaktivt diagram med data */}
      {data.map((point) => (
        <div key={point.month} style={{ height: `${point.value}px` }}>
          {point.month}: {point.value}
        </div>
      ))}
    </div>
  );
}

Genom att skicka ett Promise från Server Component till Client Component kan datahämtningen börja på servern medan klientkomponenten hydreras. use-hooken tar hand om att vänta på datan. Resultatet: ingen onödig fördröjning.

Kombinera Server Components med React Query

Server Components är fantastiska för initial datahämtning, men för data som behöver uppdateras i realtid — chattar, notifikationer, live-dashboards — behöver du något mer. Här kommer React Query (TanStack Query) in i bilden.

Mönstret är elegant: Server Component hämtar initiala data, och React Query tar över på klienten.

// app/providers.tsx
'use client';

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useState } from 'react';

export function Providers({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(() => new QueryClient());
  return (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  );
}

// app/notifications/page.tsx (Server Component)
import {
  dehydrate,
  HydrationBoundary,
  QueryClient,
} from '@tanstack/react-query';
import { NotificationList } from './NotificationList';

export default async function NotificationsPage() {
  const queryClient = new QueryClient();

  await queryClient.prefetchQuery({
    queryKey: ['notifications'],
    queryFn: () => db.notification.findMany({ take: 20 }),
  });

  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <NotificationList />
    </HydrationBoundary>
  );
}

// app/notifications/NotificationList.tsx (Client Component)
'use client';

import { useQuery } from '@tanstack/react-query';

export function NotificationList() {
  const { data: notifications, isLoading } = useQuery({
    queryKey: ['notifications'],
    queryFn: () => fetch('/api/notifications').then((r) => r.json()),
    refetchInterval: 30000, // Uppdatera var 30:e sekund
  });

  if (isLoading) return <p>Laddar...</p>;

  return (
    <ul>
      {notifications?.map((n: Notification) => (
        <li key={n.id}>{n.message}</li>
      ))}
    </ul>
  );
}

Initiala data hämtas på servern — snabbt och SEO-vänligt. React Query tar sedan över och håller datan fräsch via polling. Användaren ser aldrig ett laddningstillstånd vid den initiala sidladdningen, vilket ger en riktigt polerad upplevelse.

Vanliga fallgropar och felsökning

Cachning i Next.js kan vara... frustrerande. Låt oss gå igenom de vanligaste problemen så att du slipper göra samma misstag.

1. Data uppdateras inte efter mutation

Det här är klassikern. Du har uppdaterat data i databasen men sidan visar fortfarande gammal information. Lösningen är nästan alltid densamma — du har glömt att anropa revalidateTag eller revalidatePath i din Server Action:

'use server';
import { revalidatePath } from 'next/cache';

export async function updateProduct(id: string, data: ProductData) {
  await db.product.update({ where: { id }, data });
  revalidatePath(`/products/${id}`); // Glöm inte detta!
}

2. Sidan är dynamisk när den borde vara statisk

Om du av misstag använder cookies(), headers() eller searchParams i en komponent som egentligen inte behöver dem, kommer hela routen att bli dynamisk. Det kan vara svårt att upptäcka, men kolla build-outputen — Next.js visar vilka routes som är statiska vs dynamiska.

Flytta dynamiska anrop till client-komponenter eller Suspense-gränser istället.

3. Route Segment Config-optioner

Ibland behöver du ta kontrollen helt och explicit styra beteendet:

// Tvinga statisk rendering
export const dynamic = 'force-static';

// Tvinga dynamisk rendering
export const dynamic = 'force-dynamic';

// Revalidering var 60:e sekund
export const revalidate = 60;

// Stäng av revalidering (statisk)
export const revalidate = false;

4. Cache fungerar annorlunda i dev vs produktion

Det här har sannolikt förvirrat alla Next.js-utvecklare minst en gång. I utvecklingsläge stänger Next.js av de flesta cachlager för att du alltid ska se senaste ändringarna. Testa alltid cachningsbeteende med next build && next start innan du drar slutsatser.

Komplett exempel: E-handelsapplikation

Dags att knyta ihop allt. Låt oss kombinera alla tekniker i ett realistiskt scenario — en produktkatalog med cachning, ISR och on-demand revalidering:

// lib/products.ts
import { cacheTag, cacheLife } from 'next/cache';

export async function getCategories() {
  'use cache';
  cacheLife('days');
  cacheTag('categories');

  return db.category.findMany({
    orderBy: { sortOrder: 'asc' },
  });
}

export async function getProductsByCategory(categorySlug: string) {
  'use cache';
  cacheLife('hours');
  cacheTag('products', `category-${categorySlug}`);

  return db.product.findMany({
    where: {
      category: { slug: categorySlug },
      published: true,
    },
    include: { images: { take: 1 } },
    orderBy: { createdAt: 'desc' },
  });
}

// app/shop/[category]/page.tsx
import { Suspense } from 'react';
import { getCategories, getProductsByCategory } from '@/lib/products';
import { notFound } from 'next/navigation';

export async function generateStaticParams() {
  const categories = await getCategories();
  return categories.map((c) => ({ category: c.slug }));
}

export default async function CategoryPage({
  params,
}: {
  params: Promise<{ category: string }>;
}) {
  const { category } = await params;
  const products = await getProductsByCategory(category);

  if (products.length === 0) notFound();

  return (
    <main>
      <h1>{category}</h1>
      <div className="product-grid">
        {products.map((product) => (
          <article key={product.id}>
            <h2>{product.name}</h2>
            <p>{product.price} kr</p>
          </article>
        ))}
      </div>
    </main>
  );
}

// app/actions/products.ts
'use server';

import { revalidateTag } from 'next/cache';

export async function updateProductPrice(id: string, newPrice: number) {
  const product = await db.product.update({
    where: { id },
    data: { price: newPrice },
    include: { category: true },
  });

  // Ogiltigförklara relevant cachad data
  revalidateTag(`product-${id}`);
  revalidateTag(`category-${product.category.slug}`);
  revalidateTag('products');
}

I det här exemplet cachas kategorier i dagar (de ändras sällan), produktlistor i timmar, och enskilda produkter i dagar. När en produkt uppdateras via en Server Action ogiltigförklaras relevant cachad data automatiskt via taggar. Elegant och effektivt.

Vanliga frågor (FAQ)

Vad är skillnaden mellan SSR, SSG och ISR i Next.js App Router?

SSR (Server-Side Rendering) renderar sidan vid varje förfrågan — använd dynamic = 'force-dynamic' eller dynamiska API:er. SSG (Static Site Generation) genererar sidan vid byggtid — standardbeteendet för statiska routes. ISR (Incremental Static Regeneration) är SSG med möjlighet att uppdatera sidor utan ombygge — konfigureras via revalidate. I App Router är dessa inte separata API:er längre utan konfigurationer som styr samma renderingsmotor.

Varför cachas inte mina fetch-anrop i Next.js 15?

Från och med Next.js 15 cachas fetch-anrop inte som standard. Det är en medveten designändring jämfört med Next.js 14. Du behöver explicit aktivera cachning med cache: 'force-cache', next: { revalidate: N }, eller det nya use cache-direktivet. Tanken är att ge utvecklare full kontroll istället för överraskande implicit cachning.

Hur fungerar use cache-direktivet i Next.js 16?

use cache är ett direktiv som markerar en sida, komponent eller funktion som cachbar. Till skillnad från fetch-specifik cachning fungerar det med alla typer av asynkrona operationer — databasanrop, filoperationer, beräkningar. Du styr livslängden med cacheLife() och kan tagga poster med cacheTag() för on-demand-ogiltigförklaring. Aktivera det med cacheComponents: true i next.config.ts.

Ska jag använda revalidateTag eller revalidatePath?

Använd revalidateTag när samma data visas på flera sidor — det ogiltigförklarar alla cache-poster med den taggen. Använd revalidatePath när du vill ogiltigförklara en specifik URL. I de flesta produktionsappar behöver du båda: taggar för granulär datainvalidering och sökvägar för sidinvalidering.

Kan jag använda React Query tillsammans med Server Components?

Absolut, och det är faktiskt ett rekommenderat mönster för data som behöver realtidsuppdateringar. Hämta initiala data i en Server Component med prefetchQuery, dehydrera state med HydrationBoundary, och låt React Query ta över på klienten. Resultatet: ingen laddningsspinner vid första sidladdningen, men datan hålls fräsch via polling eller WebSockets.

Om Författaren Editorial Team

Our team of expert writers and editors.