vercel / vercel/next.js

`use cache` form action misses cache after hydrated reload

Open
#96,803 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Server Actions
Dominant language
JavaScript
Stars
142k
Forks
32.4k
Avg merge
2d 14h
Merged PRs (30d)
351

Description

Link to the code that reproduces this issue

https://github.com/hi-ogawa/reproductions/tree/main/next-use-cache-form-reload

To Reproduce

The primary scenario is:

  1. Load /inline directly and wait for hydration.
  2. Select Reset cache.
  3. Submit the cached form with message=hello.
  4. Confirm the cached function executed once.
  5. Reload the page and wait for hydration.
  6. Submit the unchanged form again.
  7. Observe that the cached function executes a second time.

The relevant source is equivalent to:

const captured = 'captured'

async function cachedAction(formData: FormData) {
  'use cache'
  // it shows logs for each form action after reload and hydration
  process.stdout.write(
    `cachedAction ${captured} ${JSON.stringify([...formData])}\n`,
  )
}

<form action={cachedAction}>
  <input name="message" defaultValue="hello" />
  <button>Submit</button>
</form>

The reproduction uses process.stdout.write() because cached functions replay captured console output and it can be misleading for verifying the original function execution.

In the reproduction project, the behavior can be confirmed by:

pnpm install
pnpm test:dev
pnpm test:build
Current vs. Expected behavior

The same result occurs in development and production:

Form path Inline directive File directive
Direct SSR load, hydrated reload 2 executions 1 execution
Client navigation, then hydrated reload 2 executions 2 executions
Direct SSR load, JavaScript disabled 1 execution 1 execution

The source-level inputs do not change: the inline capture remains captured and the application FormData remains message=hello. I expect the second submission to reuse the first cache entry in every case.

The cache miss appears to come from React's $ACTION_* transport controls becoming part of the FormData cache argument:

  • A freshly rendered inline action has $ACTION_REF_* metadata containing newly encrypted bound captures, so its value changes after reload.
  • A file-level action has stable $ACTION_ID_* metadata, which explains why the direct SSR case hits.
  • Client navigation initially submits only application fields, while the hydrated SSR form after reload also submits action controls, so even the file-level action misses.
  • Native no-JavaScript submissions pass through decodeAction(), which removes the action controls before invoking the cached function, so both directive forms hit.
Provide environment information
Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 25.5.0
  Available memory (MB): 49152
  Available CPU cores: 12
Binaries:
  Node: 24.19.0
  npm: 11.17.0
  Yarn: N/A
  pnpm: 10.34.5
Relevant Packages:
  next: 16.3.0
  eslint-config-next: N/A
  react: 19.2.8
  react-dom: 19.2.8
  typescript: 5.9.3
Next.js Config:
  output: N/A
Which area(s) are affected? (Select all that apply)

Use Cache, Server Actions

Which stage(s) are affected? (Select all that apply)

next dev (local), next build (local), next start (local)

Additional context

The same issue was reproduced on @vitejs/plugin-rsc demo in https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-rsc/examples/use-cache-callable and it appears the fix would require framework side cache key derivation to somehow exclude $ACTION_ related keys.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the reproduction at next-use-cache-form-reload and run pnpm test:dev and pnpm test:build. Trace the form-data handling around React's decodeAction() and framework cache-key derivation, especially $ACTION_* fields. Done means hydrated reload submissions reuse the cache entry for both inline and file-level actions across the listed scenarios.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, nextjs, react
Domain
backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.