Skip to content

Commit 0ffea65

Browse files
ijjkabhi12299
andauthored
Fix revalidateTag() behaviour when invoked in server components (#70446) (#70642)
This backports #70446 to 14-2-1 Fixes #70403 ### What? When `revalidateTag()` is called directly in server components, the cache is not purged for the corresponding tags. Reproduction steps available in #70403 ### How? Check [app-render.tsx](https://github.com/vercel/next.js/compare/canary...abhi12299:fix-revalidatetag-rsc?expand=1#diff-a3e2e024db1faa1b501e0dd6040eaaf0d931cb9878ae0fb0f4c3658daa982768) This issue was introduced in #65296 in this file: [revalidate.ts](https://github.com/vercel/next.js/pull/65296/files#diff-7f0cb5bb30d44b9153d724e31c25859b9aab6cc258b35563a1d9464cd0688283). The lines removed from the file resulted in the revalidation checks to be skipped when there is an RSC request. Also fixed checks on `pendingRevalidates` to also check for `revalidatedTags`. Co-authored-by: Abhishek Mehandiratta <36722596+abhi12299@users.noreply.github.com>
1 parent 190faf4 commit 0ffea65

File tree

9 files changed

+164
-5
lines changed

9 files changed

+164
-5
lines changed

packages/next/src/server/app-render/app-render.tsx

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,23 @@ async function generateFlight(
348348
}
349349
)
350350

351-
return new FlightRenderResult(flightReadableStream)
351+
const resultOptions: RenderResultOptions = {
352+
metadata: {},
353+
}
354+
355+
if (
356+
ctx.staticGenerationStore.pendingRevalidates ||
357+
ctx.staticGenerationStore.revalidatedTags
358+
) {
359+
resultOptions.waitUntil = Promise.all([
360+
ctx.staticGenerationStore.incrementalCache?.revalidateTag(
361+
ctx.staticGenerationStore.revalidatedTags || []
362+
),
363+
...Object.values(ctx.staticGenerationStore.pendingRevalidates || {}),
364+
])
365+
}
366+
367+
return new FlightRenderResult(flightReadableStream, resultOptions)
352368
}
353369

354370
type RenderToStreamResult = {
@@ -1349,7 +1365,10 @@ async function renderToHTMLOrFlightImpl(
13491365
})
13501366

13511367
// If we have pending revalidates, wait until they are all resolved.
1352-
if (staticGenerationStore.pendingRevalidates) {
1368+
if (
1369+
staticGenerationStore.pendingRevalidates ||
1370+
staticGenerationStore.revalidatedTags
1371+
) {
13531372
options.waitUntil = Promise.all([
13541373
staticGenerationStore.incrementalCache?.revalidateTag(
13551374
staticGenerationStore.revalidatedTags || []
Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
import { RSC_CONTENT_TYPE_HEADER } from '../../client/components/app-router-headers'
2-
import RenderResult from '../render-result'
2+
import RenderResult, { type RenderResultOptions } from '../render-result'
33

44
/**
55
* Flight Response is always set to RSC_CONTENT_TYPE_HEADER to ensure it does not get interpreted as HTML.
66
*/
77
export class FlightRenderResult extends RenderResult {
8-
constructor(response: string | ReadableStream<Uint8Array>) {
9-
super(response, { contentType: RSC_CONTENT_TYPE_HEADER, metadata: {} })
8+
constructor(
9+
response: string | ReadableStream<Uint8Array>,
10+
options?: RenderResultOptions
11+
) {
12+
super(response, {
13+
contentType: RSC_CONTENT_TYPE_HEADER,
14+
waitUntil: options?.waitUntil,
15+
metadata: options?.metadata ?? {},
16+
})
1017
}
1118
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
'use client'
2+
3+
import { revalidate } from './actions/revalidate'
4+
5+
export default function RevalidateViaForm({ tag }: { tag: string }) {
6+
const handleRevalidate = async () => {
7+
await revalidate(tag)
8+
}
9+
10+
return (
11+
<form action={handleRevalidate}>
12+
<button type="submit" id="submit-form" className="underline">
13+
Revalidate via form
14+
</button>
15+
</form>
16+
)
17+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
'use server'
2+
3+
import { revalidateTag } from 'next/cache'
4+
5+
export const revalidate = async (
6+
tag: string
7+
): Promise<{ revalidated: boolean }> => {
8+
revalidateTag(tag)
9+
10+
return { revalidated: true }
11+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { ReactNode } from 'react'
2+
3+
export default function Root({ children }: { children: ReactNode }) {
4+
return (
5+
<html>
6+
<body>{children}</body>
7+
</html>
8+
)
9+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import RevalidateViaForm from './RevalidateViaForm'
2+
import Link from 'next/link'
3+
4+
export default async function Page() {
5+
const data = await fetch(
6+
'https://next-data-api-endpoint.vercel.app/api/random',
7+
{
8+
next: {
9+
tags: ['data'],
10+
revalidate: false,
11+
},
12+
}
13+
).then((res) => res.text())
14+
15+
return (
16+
<div>
17+
<span id="data">{data}</span>
18+
<RevalidateViaForm tag="data" />
19+
<Link href="/revalidate_via_page?tag=data" id="revalidate-via-page">
20+
Revalidate via page
21+
</Link>
22+
</div>
23+
)
24+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
'use server'
2+
3+
import Link from 'next/link'
4+
import { revalidateTag } from 'next/cache'
5+
6+
const RevalidateViaPage = async ({
7+
searchParams,
8+
}: {
9+
searchParams: Promise<{ tag: string }>
10+
}) => {
11+
const { tag } = await searchParams
12+
revalidateTag(tag)
13+
14+
return (
15+
<div className="flex flex-col items-center justify-center h-screen">
16+
<pre>Tag [{tag}] has been revalidated</pre>
17+
<Link href="/" id="home">
18+
To Home
19+
</Link>
20+
</div>
21+
)
22+
}
23+
24+
export default RevalidateViaPage
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/**
2+
* @type {import('next').NextConfig}
3+
*/
4+
const nextConfig = {}
5+
6+
module.exports = nextConfig
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { nextTestSetup } from 'e2e-utils'
2+
import { retry } from 'next-test-utils'
3+
4+
describe('revalidateTag-rsc', () => {
5+
const { next } = nextTestSetup({
6+
files: __dirname,
7+
})
8+
9+
it('should revalidate fetch cache if revalidateTag invoked via server action', async () => {
10+
const browser = await next.browser('/')
11+
const randomNumber = await browser.elementById('data').text()
12+
await browser.refresh()
13+
const randomNumber2 = await browser.elementById('data').text()
14+
expect(randomNumber).toEqual(randomNumber2)
15+
16+
await browser.elementByCss('#submit-form').click()
17+
18+
await retry(async () => {
19+
const randomNumber3 = await browser.elementById('data').text()
20+
expect(randomNumber3).not.toEqual(randomNumber)
21+
})
22+
})
23+
24+
it('should revalidate fetch cache if revalidateTag invoked via server component', async () => {
25+
const browser = await next.browser('/')
26+
const randomNumber = await browser.elementById('data').text()
27+
await browser.refresh()
28+
const randomNumber2 = await browser.elementById('data').text()
29+
expect(randomNumber).toEqual(randomNumber2)
30+
31+
await browser.elementByCss('#revalidate-via-page').click()
32+
// need to refresh to evict client router cache
33+
await browser.waitForElementByCss('#home')
34+
await browser.refresh()
35+
36+
await browser.elementByCss('#home').click()
37+
38+
await browser.waitForElementByCss('#data')
39+
const randomNumber3 = await browser.elementById('data').text()
40+
expect(randomNumber3).not.toEqual(randomNumber)
41+
})
42+
})

0 commit comments

Comments
 (0)