Wszystkie wpisy

TIL: Next.js nie duplikuje zapytań fetch w RSC

App router istnieje od grudnia 2022 roku.

Dwie rzeczy o Next.js, które dziś w końcu do mnie dotarły.

1. Ten sam fetch w wielu Server Components = JEDNO zapytanie

Jeśli kilka Server Components w trakcie tego samego renderu odpala fetch() z tym samym URL-em i opcjami, Next automatycznie to deduplikuje. Nie ważne, że masz to w trzech różnych komponentach — request poleci tylko raz.

// components/user-name.tsx
async function UserName({ id }: { id: string }) {
  const res = await fetch(`https://api.example.com/users/${id}`);
  const user = await res.json();
  return <span>{user.name}</span>;
}
 
// components/user-avatar.tsx
async function UserAvatar({ id }: { id: string }) {
  // ten sam URL
  const res = await fetch(`https://api.example.com/users/${id}`);
  const user = await res.json();
  return <img src={user.avatarUrl} alt={user.name} />;
}
 
// page.tsx
export default function Page({ params }: { params: { id: string } }) {
  return (
    <div>
      <UserAvatar id={params.id} />
      <UserName id={params.id} />
    </div>
  );
}

Mimo że fetch odpala się w dwóch niezależnych komponentach, w Network zobaczysz tylko jedno żądanie do /users/:id. To działa dzięki request memoization — cache trzyma się tylko przez czas jednego renderu (server-side fetch cache w React/Next), nie między requestami.

2. Rewalidacja cache NIE resetuje stanu

Byłem przekonany, że po inwalidacji danych np. przez cacheTag + revalidateTag w Next 16+, Server Component się “remountuje” od zera i zeruje cały stan pod nim.

// actions.ts
'use server';
import { revalidateTag } from 'next/cache';
 
export async function updatePrice(id: string, price: number) {
  await db.products.update(id, { price });
  revalidateTag(`product-${id}`);
}
// product-price.tsx (Server Component)
async function ProductPrice({ id }: { id: string }) {
  'use cache';
  cacheTag(`product-${id}`);
  const product = await getProduct(id);
  return <PriceWidget price={product.price} />;
}
 
// price-widget.tsx (Client Component)
'use client';
function PriceWidget({ price }: { price: number }) {
  const [expanded, setExpanded] = useState(false); // <- ten stan PRZEŻYWA rerender
  return (
    <div onClick={() => setExpanded(!expanded)}>
      {price} zł {expanded && '(szczegóły...)'}
    </div>
  );
}

TL;DR: RSC rerender ≠ remount. Cache tagi inwalidują dane, nie state.

Po revalidateTag('product-123') React po prostu re-renderuje drzewo Server Components z nowymi danymi i scala wynik z istniejącym drzewem, czyli dokładnie tak jak normalna rekoncyliacja w Reakcie. Client Component w środku (jego useState, useRef itd.) zostaje przy życiu, bo jego pozycja w drzewie się nie zmienia. Zmienia się tylko price jako prop.

Wychodzi na to, że RSC mają być szkieletem danych. Nie było to dla mnie oczywiste.

Im dłużej z tym siedzę, tym bardziej mnie Next przekonuje jako framework, ale naprawdę przydałby się hard reset: wywalić przestarzałe API (Pages Router jako legacy-only, stare konwencje data fetchingu) i zostawić jeden, oficjalny “domyślny” sposób pisania aplikacji zamiast czterech konkurujących ze sobą podejść.