Skip to content

Commit

Permalink
Fix revalidateTag() behaviour when invoked in server components (#7…
Browse files Browse the repository at this point in the history
…0446)

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: JJ Kasper <[email protected]>
  • Loading branch information
abhi12299 and ijjk authored Sep 30, 2024
1 parent b8d1ef7 commit cdb78b4
Show file tree
Hide file tree
Showing 8 changed files with 149 additions and 2 deletions.
22 changes: 20 additions & 2 deletions packages/next/src/server/app-render/app-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,22 @@ async function generateDynamicFlightRenderResult(
onError,
}
)
await waitAtLeastOneReactRenderTask()

if (
ctx.staticGenerationStore.pendingRevalidates ||
ctx.staticGenerationStore.revalidatedTags ||
ctx.staticGenerationStore.pendingRevalidateWrites
) {
const promises = Promise.all([
ctx.staticGenerationStore.incrementalCache?.revalidateTag(
ctx.staticGenerationStore.revalidatedTags || []
),
...Object.values(ctx.staticGenerationStore.pendingRevalidates || {}),
...(ctx.staticGenerationStore.pendingRevalidateWrites || []),
])
ctx.renderOpts.waitUntil = (p) => promises.then(() => p)
}

return new FlightRenderResult(flightReadableStream, {
fetchMetrics: ctx.staticGenerationStore.fetchMetrics,
Expand Down Expand Up @@ -1086,7 +1102,8 @@ async function renderToHTMLOrFlightImpl(
// If we have pending revalidates, wait until they are all resolved.
if (
staticGenerationStore.pendingRevalidates ||
staticGenerationStore.pendingRevalidateWrites
staticGenerationStore.pendingRevalidateWrites ||
staticGenerationStore.revalidatedTags
) {
options.waitUntil = Promise.all([
staticGenerationStore.incrementalCache?.revalidateTag(
Expand Down Expand Up @@ -1195,7 +1212,8 @@ async function renderToHTMLOrFlightImpl(
// If we have pending revalidates, wait until they are all resolved.
if (
staticGenerationStore.pendingRevalidates ||
staticGenerationStore.pendingRevalidateWrites
staticGenerationStore.pendingRevalidateWrites ||
staticGenerationStore.revalidatedTags
) {
options.waitUntil = Promise.all([
staticGenerationStore.incrementalCache?.revalidateTag(
Expand Down
17 changes: 17 additions & 0 deletions test/e2e/app-dir/revalidatetag-rsc/app/RevalidateViaForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use client'

import { revalidate } from './actions/revalidate'

export default function RevalidateViaForm({ tag }: { tag: string }) {
const handleRevalidate = async () => {
await revalidate(tag)
}

return (
<form action={handleRevalidate}>
<button type="submit" id="submit-form" className="underline">
Revalidate via form
</button>
</form>
)
}
11 changes: 11 additions & 0 deletions test/e2e/app-dir/revalidatetag-rsc/app/actions/revalidate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
'use server'

import { revalidateTag } from 'next/cache'

export const revalidate = async (
tag: string
): Promise<{ revalidated: boolean }> => {
revalidateTag(tag)

return { revalidated: true }
}
9 changes: 9 additions & 0 deletions test/e2e/app-dir/revalidatetag-rsc/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ReactNode } from 'react'

export default function Root({ children }: { children: ReactNode }) {
return (
<html>
<body>{children}</body>
</html>
)
}
24 changes: 24 additions & 0 deletions test/e2e/app-dir/revalidatetag-rsc/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import RevalidateViaForm from './RevalidateViaForm'
import Link from 'next/link'

export default async function Page() {
const data = await fetch(
'https://next-data-api-endpoint.vercel.app/api/random',
{
next: {
tags: ['data'],
revalidate: false,
},
}
).then((res) => res.text())

return (
<div>
<span id="data">{data}</span>
<RevalidateViaForm tag="data" />
<Link href="/revalidate_via_page?tag=data" id="revalidate-via-page">
Revalidate via page
</Link>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use server'

import Link from 'next/link'
import { revalidateTag } from 'next/cache'

const RevalidateViaPage = async ({
searchParams,
}: {
searchParams: Promise<{ tag: string }>
}) => {
const { tag } = await searchParams
revalidateTag(tag)

return (
<div className="flex flex-col items-center justify-center h-screen">
<pre>Tag [{tag}] has been revalidated</pre>
<Link href="/" id="home">
To Home
</Link>
</div>
)
}

export default RevalidateViaPage
6 changes: 6 additions & 0 deletions test/e2e/app-dir/revalidatetag-rsc/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {}

module.exports = nextConfig
38 changes: 38 additions & 0 deletions test/e2e/app-dir/revalidatetag-rsc/revalidatetag-rsc.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { nextTestSetup } from 'e2e-utils'
import { retry } from 'next-test-utils'

describe('revalidateTag-rsc', () => {
const { next } = nextTestSetup({
files: __dirname,
})

it('should revalidate fetch cache if revalidateTag invoked via server action', async () => {
const browser = await next.browser('/')
const randomNumber = await browser.elementById('data').text()
await browser.refresh()
const randomNumber2 = await browser.elementById('data').text()
expect(randomNumber).toEqual(randomNumber2)

await browser.elementByCss('#submit-form').click()

await retry(async () => {
const randomNumber3 = await browser.elementById('data').text()
expect(randomNumber3).not.toEqual(randomNumber)
})
})

it('should revalidate fetch cache if revalidateTag invoked via server component', async () => {
const browser = await next.browser('/')
const randomNumber = await browser.elementById('data').text()
await browser.refresh()
const randomNumber2 = await browser.elementById('data').text()
expect(randomNumber).toEqual(randomNumber2)

await browser.elementByCss('#revalidate-via-page').click()
await browser.waitForElementByCss('#home')
await browser.elementByCss('#home').click()
await browser.waitForElementByCss('#data')
const randomNumber3 = await browser.elementById('data').text()
expect(randomNumber3).not.toEqual(randomNumber)
})
})

0 comments on commit cdb78b4

Please sign in to comment.