Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix revalidateTag() behaviour when invoked in server components (#70446) #70642

Merged
merged 4 commits into from
Sep 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
})
})
Loading