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) (#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 <[email protected]>
  • Loading branch information
ijjk and abhi12299 authored Sep 30, 2024
1 parent 190faf4 commit 0ffea65
Show file tree
Hide file tree
Showing 9 changed files with 164 additions and 5 deletions.
23 changes: 21 additions & 2 deletions packages/next/src/server/app-render/app-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,23 @@ async function generateFlight(
}
)

return new FlightRenderResult(flightReadableStream)
const resultOptions: RenderResultOptions = {
metadata: {},
}

if (
ctx.staticGenerationStore.pendingRevalidates ||
ctx.staticGenerationStore.revalidatedTags
) {
resultOptions.waitUntil = Promise.all([
ctx.staticGenerationStore.incrementalCache?.revalidateTag(
ctx.staticGenerationStore.revalidatedTags || []
),
...Object.values(ctx.staticGenerationStore.pendingRevalidates || {}),
])
}

return new FlightRenderResult(flightReadableStream, resultOptions)
}

type RenderToStreamResult = {
Expand Down Expand Up @@ -1349,7 +1365,10 @@ async function renderToHTMLOrFlightImpl(
})

// If we have pending revalidates, wait until they are all resolved.
if (staticGenerationStore.pendingRevalidates) {
if (
staticGenerationStore.pendingRevalidates ||
staticGenerationStore.revalidatedTags
) {
options.waitUntil = Promise.all([
staticGenerationStore.incrementalCache?.revalidateTag(
staticGenerationStore.revalidatedTags || []
Expand Down
13 changes: 10 additions & 3 deletions packages/next/src/server/app-render/flight-render-result.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import { RSC_CONTENT_TYPE_HEADER } from '../../client/components/app-router-headers'
import RenderResult from '../render-result'
import RenderResult, { type RenderResultOptions } from '../render-result'

/**
* Flight Response is always set to RSC_CONTENT_TYPE_HEADER to ensure it does not get interpreted as HTML.
*/
export class FlightRenderResult extends RenderResult {
constructor(response: string | ReadableStream<Uint8Array>) {
super(response, { contentType: RSC_CONTENT_TYPE_HEADER, metadata: {} })
constructor(
response: string | ReadableStream<Uint8Array>,
options?: RenderResultOptions
) {
super(response, {
contentType: RSC_CONTENT_TYPE_HEADER,
waitUntil: options?.waitUntil,
metadata: options?.metadata ?? {},
})
}
}
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
42 changes: 42 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,42 @@
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()
// need to refresh to evict client router cache
await browser.waitForElementByCss('#home')
await browser.refresh()

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 0ffea65

Please sign in to comment.