Environment
Next.js 15.4.0-canary.109 (App Router, running with next dev --turbopack)
Node.js 22.17.0
Experimental partial prerendering enabled with ppr: ‘incremental’
Data comes from a Directus REST API; we’re not using the Next.js app/api routes for this entity.
What we expect
After creating/updating/deleting a customer we call revalidateTag(‘customersSICC’) and revalidatePath(‘/dashboard/customersSICC’), then redirect back to the list. The list view should show the fresh data immediately.
What actually happens
Go to /dashboard/customersSICC (this page uses export const dynamic = ‘force-dynamic’ and renders a wrapped in ).
Click “Crear Cliente”, fill out the form, submit.
The server action runs, writes to Directus, calls revalidateTag/revalidatePath, and redirects back.
The list view shows the old data until we manually refresh the browser. The same thing happens after edit/delete. Client-side navigation via our RefreshLink (which calls router.refresh() inside startTransition) doesn’t help.
// app/lib/actions.ts
export async function createCustomerSICC(formData: FormData) {
const name = String(formData.get('name') || '');
const body = {
status: formData.get('status'),
name,
CUIT: formData.get('CUIT'),
contacto: formData.get('contacto'),
mail: formData.get('mail'),
tel: formData.get('tel'),
mailNotif: formData.get('mailNotif'),
urlSlug: slugify(name),
};
const response = await fetch(`${DIRECTUS_URL}/items/Clientes`, {
next: { revalidate: 0 },
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
await response.json(); // Directus returns the created record
revalidateTag('customersSICC');
revalidatePath('/dashboard/customersSICC');
revalidatePath('/', 'layout');
redirect('/dashboard/customersSICC');
}
export async function deleteCustomerSICC(id: string) {
await fetch(`${DIRECTUS_URL}/items/Clientes/${id}`, {
next: { revalidate: 0 },
method: 'DELETE',
});
revalidateTag('customersSICC');
revalidatePath('/dashboard/customersSICC');
redirect('/dashboard/customersSICC');
}
// app/lib/data.ts
const ITEMS_PER_PAGE = 6;
export async function getCustomersSICC(query = "", currentPage = 1) {
const url = `${DIRECTUS_URL}/items/Clientes?page=${currentPage}&limit=${ITEMS_PER_PAGE}&sort=name${
query ? `&filter[name][_contains]=${encodeURIComponent(query)}` : ''
}`;
try {
const res = await fetch(url, {
next: { tags: ['customersSICC'] },
});
if (!res.ok) throw new Error('Failed to fetch customers');
const data = await res.json();
return data.data;
} catch (err) {
console.error('Fetch customers failed', err);
return [];
}
}
The list page simply calls getCustomersSICC(query, currentPage)
inside the server component and renders the result. Pagination counts come from a similar Directus request using the same tag (that request is cache: 'no-store'
).
Things we’ve tried
-
Ensured the page is dynamic (
export const dynamic = 'force-dynamic'
). -
Added
revalidatePath('/', 'layout')
for good measure. -
Confirmed
redirect('/dashboard/customersSICC')
happens after the revalidation calls. -
Wrapped our navigation buttons with a custom
RefreshLink
that doesrouter.push(...)
+router.refresh()
insidestartTransition
. -
Verified that Directus receives the mutation and returns the new record ID.
Still, the data in the table is stale until a hard refresh. Are we missing something about how revalidateTag
interacts with fetches that omit cache: 'no-store'
in Next 15? Could partial prerendering be keeping the old snapshot around even with force-dynamic
? Any guidance on diagnosing this would be hugely appreciated!