From 6b1fa43f53b2a5572608ff2abe0bcdd3addcd8ca Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Thu, 4 Jan 2024 14:01:50 +0100 Subject: [PATCH] Change server actions cache default to no-store (#60170) ## What? Currently there is a bug in Server Actions when you `fetch` as it uses the same defaults (caching when not specified) as rendering, this causes some issues as you want to read your writes in Server Actions. This change adds the `no-store` default for Server Actions, you can still override it by specifying `cache: 'force-cache'` for example, but it defaults to `cache: 'no-store'`. Fixes NEXT-1926 --------- Co-authored-by: Zack Tanner --- .../src/server/app-render/action-handler.ts | 3 + test/e2e/app-dir/actions/app-action.test.ts | 72 +++++++++++++++++++ .../app/no-caching-in-actions/actions.js | 9 +++ .../force-cache/actions.js | 12 ++++ .../no-caching-in-actions/force-cache/page.js | 27 +++++++ .../actions/app/no-caching-in-actions/page.js | 27 +++++++ .../revalidate/actions.js | 14 ++++ .../no-caching-in-actions/revalidate/page.js | 27 +++++++ 8 files changed, 191 insertions(+) create mode 100644 test/e2e/app-dir/actions/app/no-caching-in-actions/actions.js create mode 100644 test/e2e/app-dir/actions/app/no-caching-in-actions/force-cache/actions.js create mode 100644 test/e2e/app-dir/actions/app/no-caching-in-actions/force-cache/page.js create mode 100644 test/e2e/app-dir/actions/app/no-caching-in-actions/page.js create mode 100644 test/e2e/app-dir/actions/app/no-caching-in-actions/revalidate/actions.js create mode 100644 test/e2e/app-dir/actions/app/no-caching-in-actions/revalidate/page.js diff --git a/packages/next/src/server/app-render/action-handler.ts b/packages/next/src/server/app-render/action-handler.ts index eafba6c75866d..b596798897083 100644 --- a/packages/next/src/server/app-render/action-handler.ts +++ b/packages/next/src/server/app-render/action-handler.ts @@ -299,6 +299,9 @@ export async function handleAction({ ) } + // When running actions the default is no-store, you can still `cache: 'force-cache'` + staticGenerationStore.fetchCache = 'default-no-store' + const originDomain = typeof req.headers['origin'] === 'string' ? new URL(req.headers['origin']).host diff --git a/test/e2e/app-dir/actions/app-action.test.ts b/test/e2e/app-dir/actions/app-action.test.ts index b4698bf084fa1..620aabf344345 100644 --- a/test/e2e/app-dir/actions/app-action.test.ts +++ b/test/e2e/app-dir/actions/app-action.test.ts @@ -1114,5 +1114,77 @@ createNextDescribe( }) }) }) + + describe('caching disabled by default', () => { + it('should use no-store as default for server action', async () => { + const browser = await next.browser('/no-caching-in-actions') + await browser + .waitForElementByCss('#trigger-fetch') + .click() + .waitForElementByCss('#fetched-data') + + const getNumber = async () => + JSON.parse(await browser.elementByCss('#fetched-data').text()) + + const firstNumber = await getNumber() + + await browser.waitForElementByCss('#trigger-fetch').click() + + await check(async () => { + const newNumber = await getNumber() + // Expect that the number changes on each click + expect(newNumber).not.toBe(firstNumber) + + return 'success' + }, 'success') + }) + + it('should not override force-cache in server action', async () => { + const browser = await next.browser('/no-caching-in-actions/force-cache') + await browser + .waitForElementByCss('#trigger-fetch') + .click() + .waitForElementByCss('#fetched-data') + + const getNumber = async () => + JSON.parse(await browser.elementByCss('#fetched-data').text()) + + const firstNumber = await getNumber() + + await browser.waitForElementByCss('#trigger-fetch').click() + + await check(async () => { + const newNumber = await getNumber() + // Expect that the number is the same on each click + expect(newNumber).toBe(firstNumber) + + return 'success' + }, 'success') + }) + + // Implicit force-cache + it('should not override revalidate in server action', async () => { + const browser = await next.browser('/no-caching-in-actions/revalidate') + await browser + .waitForElementByCss('#trigger-fetch') + .click() + .waitForElementByCss('#fetched-data') + + const getNumber = async () => + JSON.parse(await browser.elementByCss('#fetched-data').text()) + + const firstNumber = await getNumber() + + await browser.waitForElementByCss('#trigger-fetch').click() + + await check(async () => { + const newNumber = await getNumber() + // Expect that the number is the same on each click + expect(newNumber).toBe(firstNumber) + + return 'success' + }, 'success') + }) + }) } ) diff --git a/test/e2e/app-dir/actions/app/no-caching-in-actions/actions.js b/test/e2e/app-dir/actions/app/no-caching-in-actions/actions.js new file mode 100644 index 0000000000000..dfe57ef1174cf --- /dev/null +++ b/test/e2e/app-dir/actions/app/no-caching-in-actions/actions.js @@ -0,0 +1,9 @@ +'use server' + +export async function getNumber() { + const res = await fetch( + 'https://next-data-api-endpoint.vercel.app/api/random?no-caching-actions' + ) + + return res.text() +} diff --git a/test/e2e/app-dir/actions/app/no-caching-in-actions/force-cache/actions.js b/test/e2e/app-dir/actions/app/no-caching-in-actions/force-cache/actions.js new file mode 100644 index 0000000000000..d9fc1bbe6c922 --- /dev/null +++ b/test/e2e/app-dir/actions/app/no-caching-in-actions/force-cache/actions.js @@ -0,0 +1,12 @@ +'use server' + +export async function getNumber() { + const res = await fetch( + 'https://next-data-api-endpoint.vercel.app/api/random?no-caching-actions', + { + cache: 'force-cache', + } + ) + + return res.text() +} diff --git a/test/e2e/app-dir/actions/app/no-caching-in-actions/force-cache/page.js b/test/e2e/app-dir/actions/app/no-caching-in-actions/force-cache/page.js new file mode 100644 index 0000000000000..50504c77cecdd --- /dev/null +++ b/test/e2e/app-dir/actions/app/no-caching-in-actions/force-cache/page.js @@ -0,0 +1,27 @@ +'use client' +import { useState, useTransition } from 'react' +import { getNumber } from './actions' + +export default function Page() { + const [isPending, startTransition] = useTransition() + const [result, setResult] = useState(null) + return ( + <> +

No Caching in Actions: {isPending ? 'pending' : 'not pending'}

+ {result !== null ? ( +
{JSON.stringify(result)}
+ ) : null} + + + ) +} diff --git a/test/e2e/app-dir/actions/app/no-caching-in-actions/page.js b/test/e2e/app-dir/actions/app/no-caching-in-actions/page.js new file mode 100644 index 0000000000000..50504c77cecdd --- /dev/null +++ b/test/e2e/app-dir/actions/app/no-caching-in-actions/page.js @@ -0,0 +1,27 @@ +'use client' +import { useState, useTransition } from 'react' +import { getNumber } from './actions' + +export default function Page() { + const [isPending, startTransition] = useTransition() + const [result, setResult] = useState(null) + return ( + <> +

No Caching in Actions: {isPending ? 'pending' : 'not pending'}

+ {result !== null ? ( +
{JSON.stringify(result)}
+ ) : null} + + + ) +} diff --git a/test/e2e/app-dir/actions/app/no-caching-in-actions/revalidate/actions.js b/test/e2e/app-dir/actions/app/no-caching-in-actions/revalidate/actions.js new file mode 100644 index 0000000000000..9759e4f4f6e97 --- /dev/null +++ b/test/e2e/app-dir/actions/app/no-caching-in-actions/revalidate/actions.js @@ -0,0 +1,14 @@ +'use server' + +export async function getNumber() { + const res = await fetch( + 'https://next-data-api-endpoint.vercel.app/api/random?no-caching-actions', + { + next: { + revalidate: 100, + }, + } + ) + + return res.text() +} diff --git a/test/e2e/app-dir/actions/app/no-caching-in-actions/revalidate/page.js b/test/e2e/app-dir/actions/app/no-caching-in-actions/revalidate/page.js new file mode 100644 index 0000000000000..50504c77cecdd --- /dev/null +++ b/test/e2e/app-dir/actions/app/no-caching-in-actions/revalidate/page.js @@ -0,0 +1,27 @@ +'use client' +import { useState, useTransition } from 'react' +import { getNumber } from './actions' + +export default function Page() { + const [isPending, startTransition] = useTransition() + const [result, setResult] = useState(null) + return ( + <> +

No Caching in Actions: {isPending ? 'pending' : 'not pending'}

+ {result !== null ? ( +
{JSON.stringify(result)}
+ ) : null} + + + ) +}