diff --git a/packages/next/cache.d.ts b/packages/next/cache.d.ts index 998b438f51fb3..2f0e6fd1f1810 100644 --- a/packages/next/cache.d.ts +++ b/packages/next/cache.d.ts @@ -1,3 +1,4 @@ export { unstable_cache } from 'next/dist/server/web/spec-extension/unstable-cache' export { revalidatePath } from 'next/dist/server/web/spec-extension/revalidate-path' export { revalidateTag } from 'next/dist/server/web/spec-extension/revalidate-tag' +export { unstable_noStore } from 'next/dist/server/web/spec-extension/unstable-no-store' diff --git a/packages/next/cache.js b/packages/next/cache.js index 0c0c589b2b748..96f73f3f8c947 100644 --- a/packages/next/cache.js +++ b/packages/next/cache.js @@ -5,6 +5,9 @@ const cacheExports = { .revalidateTag, revalidatePath: require('next/dist/server/web/spec-extension/revalidate-path') .revalidatePath, + unstable_noStore: + require('next/dist/server/web/spec-extension/unstable-no-store') + .unstable_noStore, } // https://nodejs.org/api/esm.html#commonjs-namespaces @@ -15,3 +18,4 @@ module.exports = cacheExports exports.unstable_cache = cacheExports.unstable_cache exports.revalidatePath = cacheExports.revalidatePath exports.revalidateTag = cacheExports.revalidateTag +exports.unstable_noStore = cacheExports.unstable_noStore diff --git a/packages/next/src/client/components/static-generation-async-storage.external.ts b/packages/next/src/client/components/static-generation-async-storage.external.ts index 5caec74a52ff2..89aca3a0e6687 100644 --- a/packages/next/src/client/components/static-generation-async-storage.external.ts +++ b/packages/next/src/client/components/static-generation-async-storage.external.ts @@ -12,6 +12,7 @@ export interface StaticGenerationStore { readonly isOnDemandRevalidate?: boolean readonly isPrerendering?: boolean readonly isRevalidate?: boolean + readonly isUnstableCacheCallback?: boolean forceDynamic?: boolean fetchCache?: diff --git a/packages/next/src/server/web/exports/unstable-no-store.ts b/packages/next/src/server/web/exports/unstable-no-store.ts new file mode 100644 index 0000000000000..14f55fa722c26 --- /dev/null +++ b/packages/next/src/server/web/exports/unstable-no-store.ts @@ -0,0 +1,2 @@ +// This file is for modularized imports for next/server to get fully-treeshaking. +export { unstable_noStore as default } from '../spec-extension/unstable-no-store' diff --git a/packages/next/src/server/web/spec-extension/unstable-cache.ts b/packages/next/src/server/web/spec-extension/unstable-cache.ts index 27d2e5c8f6dcb..d7d70788ea7bf 100644 --- a/packages/next/src/server/web/spec-extension/unstable-cache.ts +++ b/packages/next/src/server/web/spec-extension/unstable-cache.ts @@ -54,6 +54,7 @@ export function unstable_cache( fetchCache: 'only-no-store', urlPathname: store?.urlPathname || '/', isStaticGeneration: !!store?.isStaticGeneration, + isUnstableCacheCallback: true, }, async () => { const tags = validateTags( diff --git a/packages/next/src/server/web/spec-extension/unstable-no-store.ts b/packages/next/src/server/web/spec-extension/unstable-no-store.ts new file mode 100644 index 0000000000000..c7271aa9ed507 --- /dev/null +++ b/packages/next/src/server/web/spec-extension/unstable-no-store.ts @@ -0,0 +1,16 @@ +import { staticGenerationAsyncStorage } from '../../../client/components/static-generation-async-storage.external' +import { staticGenerationBailout } from '../../../client/components/static-generation-bailout' + +export function unstable_noStore() { + const staticGenerationStore = staticGenerationAsyncStorage.getStore() + + if (staticGenerationStore?.isUnstableCacheCallback) { + // if called within a next/cache call, we want to cache the result + // and defer to the next/cache call to handle how to cache the result. + return + } + + staticGenerationBailout('unstable_noStore', { + link: 'https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering', + }) +} diff --git a/test/e2e/app-dir/app-static/app-static.test.ts b/test/e2e/app-dir/app-static/app-static.test.ts index ede749c02a3d9..e8ec57358d386 100644 --- a/test/e2e/app-dir/app-static/app-static.test.ts +++ b/test/e2e/app-dir/app-static/app-static.test.ts @@ -710,6 +710,12 @@ createNextDescribe( 'articles/[slug]/page_client-reference-manifest.js', 'articles/works.html', 'articles/works.rsc', + 'no-store/dynamic/page.js', + 'no-store/dynamic/page_client-reference-manifest.js', + 'no-store/static.html', + 'no-store/static.rsc', + 'no-store/static/page.js', + 'no-store/static/page_client-reference-manifest.js', ].sort() ) }) @@ -1018,6 +1024,22 @@ createNextDescribe( "initialRevalidateSeconds": false, "srcRoute": "/hooks/use-search-params/with-suspense", }, + "/no-store/static": Object { + "dataRoute": "/no-store/static.rsc", + "experimentalBypassFor": Array [ + Object { + "key": "Next-Action", + "type": "header", + }, + Object { + "key": "content-type", + "type": "header", + "value": "multipart/form-data", + }, + ], + "initialRevalidateSeconds": false, + "srcRoute": "/no-store/static", + }, "/partial-gen-params-no-additional-lang/en/RAND": Object { "dataRoute": "/partial-gen-params-no-additional-lang/en/RAND.rsc", "experimentalBypassFor": Array [ @@ -2921,6 +2943,30 @@ createNextDescribe( }) }) + describe('unstable_noStore', () => { + it('should opt-out of static optimization', async () => { + const res = await next.fetch('/no-store/dynamic') + const html = await res.text() + const data = cheerio.load(html)('#uncached-data').text() + const res2 = await next.fetch('/no-store/dynamic') + const html2 = await res2.text() + const data2 = cheerio.load(html2)('#uncached-data').text() + + expect(data).not.toEqual(data2) + }) + + it('should not opt-out of static optimization when used in next/cache', async () => { + const res = await next.fetch('/no-store/static') + const html = await res.text() + const data = cheerio.load(html)('#data').text() + const res2 = await next.fetch('/no-store/static') + const html2 = await res2.text() + const data2 = cheerio.load(html2)('#data').text() + + expect(data).toEqual(data2) + }) + }) + it('should keep querystring on static page', async () => { const browser = await next.browser('/blog/tim?message=hello-world') const checkUrl = async () => diff --git a/test/e2e/app-dir/app-static/app/no-store/dynamic/page.tsx b/test/e2e/app-dir/app-static/app/no-store/dynamic/page.tsx new file mode 100644 index 0000000000000..ca34fd2c890cd --- /dev/null +++ b/test/e2e/app-dir/app-static/app/no-store/dynamic/page.tsx @@ -0,0 +1,12 @@ +import { getUncachedRandomData } from '../no-store-fn' + +export default async function Page() { + const uncachedData = await getUncachedRandomData() + + return ( +
+

random: {Math.random()}

+

uncachedData: {uncachedData.random}

+
+ ) +} diff --git a/test/e2e/app-dir/app-static/app/no-store/no-store-fn.ts b/test/e2e/app-dir/app-static/app/no-store/no-store-fn.ts new file mode 100644 index 0000000000000..14898825017f5 --- /dev/null +++ b/test/e2e/app-dir/app-static/app/no-store/no-store-fn.ts @@ -0,0 +1,8 @@ +import { unstable_noStore } from 'next/cache' + +export function getUncachedRandomData() { + unstable_noStore() + return { + random: Math.random(), + } +} diff --git a/test/e2e/app-dir/app-static/app/no-store/revalidate-button.tsx b/test/e2e/app-dir/app-static/app/no-store/revalidate-button.tsx new file mode 100644 index 0000000000000..89ff47a7d365e --- /dev/null +++ b/test/e2e/app-dir/app-static/app/no-store/revalidate-button.tsx @@ -0,0 +1,9 @@ +'use client' + +export function RevalidateButton({ onClick }) { + return ( +
+ +
+ ) +} diff --git a/test/e2e/app-dir/app-static/app/no-store/static/page.tsx b/test/e2e/app-dir/app-static/app/no-store/static/page.tsx new file mode 100644 index 0000000000000..284b1e9c36ad6 --- /dev/null +++ b/test/e2e/app-dir/app-static/app/no-store/static/page.tsx @@ -0,0 +1,28 @@ +import { revalidateTag, unstable_cache } from 'next/cache' +import { getUncachedRandomData } from '../no-store-fn' +import { RevalidateButton } from '../revalidate-button' + +export default async function Page() { + async function revalidate() { + 'use server' + await revalidateTag('no-store-fn') + } + + const cachedData = await unstable_cache( + async () => { + return getUncachedRandomData() + }, + ['random'], + { + tags: ['no-store-fn'], + } + )() + + return ( +
+

random: {Math.random()}

+

cachedData: {cachedData.random}

+ +
+ ) +} diff --git a/test/e2e/app-dir/app-static/next.config.js b/test/e2e/app-dir/app-static/next.config.js index 1091d8c819406..e473392c23ce5 100644 --- a/test/e2e/app-dir/app-static/next.config.js +++ b/test/e2e/app-dir/app-static/next.config.js @@ -4,6 +4,7 @@ module.exports = { logging: { level: 'verbose', }, + serverActions: true, incrementalCacheHandlerPath: process.env.CUSTOM_CACHE_HANDLER, },