diff --git a/packages/next/src/server/lib/patch-fetch.ts b/packages/next/src/server/lib/patch-fetch.ts
index d6152bbd0a527..5846c72f79c61 100644
--- a/packages/next/src/server/lib/patch-fetch.ts
+++ b/packages/next/src/server/lib/patch-fetch.ts
@@ -254,30 +254,25 @@ export function createPatchedFetcher(
`fetch ${input.toString()}`
)
- if (
+ const revalidateStore =
workUnitStore &&
(workUnitStore.type === 'cache' ||
workUnitStore.type === 'prerender' ||
workUnitStore.type === 'prerender-ppr' ||
workUnitStore.type === 'prerender-legacy')
- ) {
+ ? workUnitStore
+ : undefined
+
+ if (revalidateStore) {
if (Array.isArray(tags)) {
// Collect tags onto parent caches or parent prerenders.
const collectedTags =
- workUnitStore.tags ?? (workUnitStore.tags = [])
+ revalidateStore.tags ?? (revalidateStore.tags = [])
for (const tag of tags) {
if (!collectedTags.includes(tag)) {
collectedTags.push(tag)
}
}
-
- // Add tags of the current cache scope to the locally collected tags
- // for this fetch call.
- for (const tag of collectedTags) {
- if (!tags.includes(tag)) {
- tags.push(tag)
- }
- }
}
}
@@ -286,12 +281,10 @@ export function createPatchedFetcher(
? []
: workUnitStore.implicitTags
- // Inside unstable-cache or "use cache", we treat it the same as
- // force-no-store on the page.
+ // Inside unstable-cache we treat it the same as force-no-store on the
+ // page.
const pageFetchCacheMode =
- workUnitStore &&
- (workUnitStore.type === 'unstable-cache' ||
- workUnitStore.type === 'cache')
+ workUnitStore && workUnitStore.type === 'unstable-cache'
? 'force-no-store'
: workStore.fetchCache
@@ -359,15 +352,6 @@ export function createPatchedFetcher(
getRequestMeta('method')?.toLowerCase() || 'get'
)
- const revalidateStore =
- workUnitStore &&
- (workUnitStore.type === 'cache' ||
- workUnitStore.type === 'prerender' ||
- workUnitStore.type === 'prerender-ppr' ||
- workUnitStore.type === 'prerender-legacy')
- ? workUnitStore
- : undefined
-
/**
* We automatically disable fetch caching under the following conditions:
* - Fetch cache configs are not set. Specifically:
@@ -515,7 +499,9 @@ export function createPatchedFetcher(
}
}
- if (revalidateStore) {
+ // We only want to set the revalidate store's revalidate time if it
+ // was explicitly set for the fetch call, i.e. currentFetchRevalidate.
+ if (revalidateStore && currentFetchRevalidate === finalRevalidate) {
revalidateStore.revalidate = finalRevalidate
}
}
diff --git a/test/e2e/app-dir/use-cache/app/cache-tag/button.tsx b/test/e2e/app-dir/use-cache/app/cache-tag/button.tsx
deleted file mode 100644
index c08ae38faed7e..0000000000000
--- a/test/e2e/app-dir/use-cache/app/cache-tag/button.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import React from 'react'
-import { revalidateTag } from 'next/cache'
-
-export function RevalidateButtons() {
- return (
-
- )
-}
diff --git a/test/e2e/app-dir/use-cache/app/cache-tag/buttons.tsx b/test/e2e/app-dir/use-cache/app/cache-tag/buttons.tsx
new file mode 100644
index 0000000000000..59ec681bf6ceb
--- /dev/null
+++ b/test/e2e/app-dir/use-cache/app/cache-tag/buttons.tsx
@@ -0,0 +1,63 @@
+import React from 'react'
+import { revalidatePath, revalidateTag } from 'next/cache'
+
+export function RevalidateButtons() {
+ return (
+
+ )
+}
diff --git a/test/e2e/app-dir/use-cache/app/cache-tag/page.tsx b/test/e2e/app-dir/use-cache/app/cache-tag/page.tsx
index 603ce92733547..ad50c97db7db4 100644
--- a/test/e2e/app-dir/use-cache/app/cache-tag/page.tsx
+++ b/test/e2e/app-dir/use-cache/app/cache-tag/page.tsx
@@ -1,28 +1,57 @@
import React from 'react'
import { unstable_cacheTag as cacheTag } from 'next/cache'
-import { RevalidateButtons } from './button'
+import { RevalidateButtons } from './buttons'
-async function getCachedWithTag(tag: string) {
+async function getCachedWithTag({
+ tag,
+ fetchCache,
+}: {
+ tag: string
+ fetchCache?: 'force' | 'revalidate'
+}) {
'use cache'
cacheTag(tag, 'c')
+ // If `force-cache` or `revalidate` is used for the fetch call, it creates
+ // basically an inner cache, and revalidating tag 'c' won't revalidate the
+ // fetch cache. If both are not used, the fetch is not cached at all in the
+ // fetch cache, and is included in the cached result of `getCachedWithTag`
+ // instead, thus also affected by revalidating 'c'.
const response = await fetch(
- `https://next-data-api-endpoint.vercel.app/api/random?tag=${tag}`
+ `https://next-data-api-endpoint.vercel.app/api/random?tag=${tag}`,
+ {
+ cache: fetchCache === 'force' ? 'force-cache' : undefined,
+ next: { revalidate: fetchCache === 'revalidate' ? 42 : undefined },
+ }
)
- return response.text()
+ const fetchedValue = await response.text()
+
+ return [Math.random(), fetchedValue]
}
export default async function Page() {
- const x = await getCachedWithTag('a')
- const y = await getCachedWithTag('b')
+ const a = await getCachedWithTag({ tag: 'a' })
+ const b = await getCachedWithTag({ tag: 'b' })
+
+ const [f1, f2] = await getCachedWithTag({
+ tag: 'f',
+ fetchCache: 'force',
+ })
+
+ const [r1, r2] = await getCachedWithTag({
+ tag: 'r',
+ fetchCache: 'revalidate',
+ })
return (
-
{x}
-
-
{y}
-
+
[a, c] {a.join(' ')}
+
[b, c] {b.join(' ')}
+
[f, c] {f1}
+
[-] {f2}
+
[r, c] {r1}
+
[-] {r2}
)
diff --git a/test/e2e/app-dir/use-cache/use-cache.test.ts b/test/e2e/app-dir/use-cache/use-cache.test.ts
index c772e8d11e0d2..35e001ea895f5 100644
--- a/test/e2e/app-dir/use-cache/use-cache.test.ts
+++ b/test/e2e/app-dir/use-cache/use-cache.test.ts
@@ -179,30 +179,93 @@ describe('use-cache', () => {
if (!isNextDeploy) {
it('should update after revalidateTag correctly', async () => {
const browser = await next.browser('/cache-tag')
+ const initial = await browser.elementByCss('#a').text()
- const initialX = await browser.elementByCss('#x').text()
- const initialY = await browser.elementByCss('#y').text()
- let updatedX: string | undefined
- let updatedY: string | undefined
+ // Bust the ISR cache first, to populate the in-memory cache for the
+ // subsequent revalidateTag calls.
+ await browser.elementByCss('#revalidate-path').click()
+ await retry(async () => {
+ expect(await browser.elementByCss('#a').text()).not.toBe(initial)
+ })
+
+ let valueA = await browser.elementByCss('#a').text()
+ let valueB = await browser.elementByCss('#b').text()
+ let valueF1 = await browser.elementByCss('#f1').text()
+ let valueF2 = await browser.elementByCss('#f2').text()
+ let valueR1 = await browser.elementByCss('#r1').text()
+ let valueR2 = await browser.elementByCss('#r2').text()
await browser.elementByCss('#revalidate-a').click()
await retry(async () => {
- updatedX = await browser.elementByCss('#x').text()
- expect(updatedX).not.toBe(initialX)
- expect(await browser.elementByCss('#y').text()).toBe(initialY)
+ expect(await browser.elementByCss('#a').text()).not.toBe(valueA)
+ expect(await browser.elementByCss('#b').text()).toBe(valueB)
+ expect(await browser.elementByCss('#f1').text()).toBe(valueF1)
+ expect(await browser.elementByCss('#f2').text()).toBe(valueF2)
+ expect(await browser.elementByCss('#r1').text()).toBe(valueR1)
+ expect(await browser.elementByCss('#r2').text()).toBe(valueR2)
})
+ valueA = await browser.elementByCss('#a').text()
+
await browser.elementByCss('#revalidate-b').click()
await retry(async () => {
- updatedY = await browser.elementByCss('#y').text()
- expect(updatedY).not.toBe(initialY)
- expect(await browser.elementByCss('#x').text()).toBe(updatedX)
+ expect(await browser.elementByCss('#a').text()).toBe(valueA)
+ expect(await browser.elementByCss('#b').text()).not.toBe(valueB)
+ expect(await browser.elementByCss('#f1').text()).toBe(valueF1)
+ expect(await browser.elementByCss('#f2').text()).toBe(valueF2)
+ expect(await browser.elementByCss('#r1').text()).toBe(valueR1)
+ expect(await browser.elementByCss('#r2').text()).toBe(valueR2)
})
+ valueB = await browser.elementByCss('#b').text()
+
await browser.elementByCss('#revalidate-c').click()
await retry(async () => {
- expect(await browser.elementByCss('#x').text()).not.toBe(updatedX)
- expect(await browser.elementByCss('#y').text()).not.toBe(updatedY)
+ expect(await browser.elementByCss('#a').text()).not.toBe(valueA)
+ expect(await browser.elementByCss('#b').text()).not.toBe(valueB)
+ expect(await browser.elementByCss('#f1').text()).not.toBe(valueF1)
+ expect(await browser.elementByCss('#f2').text()).toBe(valueF2)
+ expect(await browser.elementByCss('#r1').text()).not.toBe(valueR1)
+ expect(await browser.elementByCss('#r2').text()).toBe(valueR2)
+ })
+
+ valueA = await browser.elementByCss('#a').text()
+ valueB = await browser.elementByCss('#b').text()
+ valueF1 = await browser.elementByCss('#f1').text()
+ valueR1 = await browser.elementByCss('#r1').text()
+
+ await browser.elementByCss('#revalidate-f').click()
+ await retry(async () => {
+ expect(await browser.elementByCss('#a').text()).toBe(valueA)
+ expect(await browser.elementByCss('#b').text()).toBe(valueB)
+ expect(await browser.elementByCss('#f1').text()).not.toBe(valueF1)
+ expect(await browser.elementByCss('#f2').text()).toBe(valueF2)
+ expect(await browser.elementByCss('#r1').text()).toBe(valueR1)
+ expect(await browser.elementByCss('#r2').text()).toBe(valueR2)
+ })
+
+ valueF1 = await browser.elementByCss('#f1').text()
+
+ await browser.elementByCss('#revalidate-r').click()
+ await retry(async () => {
+ expect(await browser.elementByCss('#a').text()).toBe(valueA)
+ expect(await browser.elementByCss('#b').text()).toBe(valueB)
+ expect(await browser.elementByCss('#f1').text()).toBe(valueF1)
+ expect(await browser.elementByCss('#f2').text()).toBe(valueF2)
+ expect(await browser.elementByCss('#r1').text()).not.toBe(valueR1)
+ expect(await browser.elementByCss('#r2').text()).toBe(valueR2)
+ })
+
+ valueR1 = await browser.elementByCss('#r1').text()
+
+ await browser.elementByCss('#revalidate-path').click()
+ await retry(async () => {
+ expect(await browser.elementByCss('#a').text()).not.toBe(valueA)
+ expect(await browser.elementByCss('#b').text()).not.toBe(valueB)
+ expect(await browser.elementByCss('#f1').text()).not.toBe(valueF1)
+ expect(await browser.elementByCss('#f2').text()).not.toBe(valueF2)
+ expect(await browser.elementByCss('#r1').text()).not.toBe(valueR1)
+ expect(await browser.elementByCss('#r2').text()).not.toBe(valueR2)
})
})
}
@@ -217,6 +280,12 @@ describe('use-cache', () => {
expect(
prerenderManifest.routes['/cache-life'].initialRevalidateSeconds
).toBe(100)
+
+ // The revalidate config from the fetch call should lower the revalidate
+ // config for the page.
+ expect(
+ prerenderManifest.routes['/cache-tag'].initialRevalidateSeconds
+ ).toBe(42)
})
it('should match the expected stale config in the page header', async () => {
@@ -230,7 +299,7 @@ describe('use-cache', () => {
const meta = JSON.parse(
await next.readFile('.next/server/app/cache-tag.meta')
)
- expect(meta.headers['x-next-cache-tags']).toContain('a,c,b')
+ expect(meta.headers['x-next-cache-tags']).toContain('a,c,b,f,r')
})
}