kit icon indicating copy to clipboard operation
kit copied to clipboard

Remote `form` with `.for()` duplicate requests exponentially

Open bhuynhdev opened this issue 4 months ago • 3 comments

Describe the bug

When I have multiple forms on the same page that submits to the same Remote function, Svelte recommends me to use a form.for(key) to distinguish between difference form instances (Don't seem to see docs for this anywhere, but the message is in console.log).

Uncaught error: A form object can only be attached to a single `<form>` element.
To create multiple instances, use `updateScore.for(key)

However, using .for() duplicates my request exponentially. First submit sends 1 requests, 2nd submit sends 2 requests, 3rd submits send 4 requests, 4th submit sends 8 requests, and so on...

What strange is that if I remove the .for() part, the Remote form looks to work like normal again.

Reproduction

Here is a Github reproduction repo: https://github.com/bhuynhdev/sveltekit-issue-14546 Here is a SvelteLab link with same content: https://www.sveltelab.dev/gpq6890y483k245?files=.%2Fsrc%2Froutes%2F%2Bpage.svelte

The reproduction step is simple:

  1. Create new project with npx sv create my-app
  2. Install valibot: npm install valibot
  3. Change svelte.config.js to include experimental features
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';

const config = {
	preprocess: vitePreprocess(),
	compilerOptions: {
		experimental: {
			async: true
		},
	},
	kit: {
		adapter: adapter(),
		experimental: {
      remoteFunctions: true
    },
	},
};

export default config;
  1. Create routes/data.remote.js with following content:
import * as v from 'valibot'
import { form, query } from '$app/server'
let SCORE = {
  "amy": 1,
	// "bob": 2,
};

export const listScore = query(async () => {
  const dataAsPromise = await new Promise(resolve => resolve(Object.entries(SCORE)))
  return dataAsPromise
})

export const updateScore = form(
	v.object({name: v.picklist(Object.keys(SCORE))}),
  async (form) => {
    SCORE[form.name] = Number(SCORE[form.name]) + 1
    await listScore().refresh()
  }
)
  1. Update page.svelte to following content:
<script>
  import { listScore, updateScore } from "./data.remote";
  const score = listScore();
</script>

<div>
  <pre>{JSON.stringify(score.current, null, 2)}</pre>
  {#each score.current || [] as [name, _], i (name)}
    <!-- enhance to prevent form reset and potentially do other thing like show toast -->
    <form
      {...updateScore.for(name).enhance(async ({ submit }) => await submit())}
    >
      <input type="hidden" name="name" value={name} />
      <button type="submit">Submit {i}</button>
    </form>
  {/each}
</div>
  1. Run the page with npm run dev
  2. Try clicking the Submit button multiple time on http://localhost:5173

Logs


System Info

System:
    OS: Linux 6.6 Ubuntu 24.04.2 LTS 24.04.2 LTS (Noble Numbat)
    CPU: (32) x64 13th Gen Intel(R) Core(TM) i9-13900HX
    Memory: 13.50 GB / 15.46 GB
    Container: Yes
    Shell: 5.2.21 - /bin/bash
  Binaries:
    Node: 22.17.0 - ~/.local/share/mise/installs/node/22.17.0/bin/node
    npm: 10.9.2 - ~/.local/share/mise/installs/node/22.17.0/bin/npm
    pnpm: 10.15.1 - ~/.local/share/pnpm/pnpm
  npmPackages:
    @sveltejs/adapter-auto: ^6.0.0 => 6.1.0
    @sveltejs/kit: ^2.22.0 => 2.43.5
    @sveltejs/vite-plugin-svelte: ^6.0.0 => 6.2.1
    svelte: ^5.0.0 => 5.39.6
    vite: ^7.0.4 => 7.1.7

Severity

blocking an upgrade

Additional Information

No response

bhuynhdev avatar Sep 27 '25 15:09 bhuynhdev

I think the problem is a combination of:

  • {#each} rerunning whenever score.current refreshes creating multiple forms in cache with the same .for() name
  • .enhance() programmatically submitting all cached forms with the designated .for() name

By removing the .enhance() it behaves as expected.

https://www.sveltelab.dev/44u25vu267qp2d7

Alternatively you should prevent the loop from rerunning by importing the names in a separate query, or if hard coded, as a separate import.

https://www.sveltelab.dev/y795sdtwg5wiki0


Also, this appears to have been previously reported: https://github.com/sveltejs/kit/issues/14498


Ultimately when the each loop, or some other effect, reruns it should clean up the existing form(s) from cache to prevent extra submissions.

sillvva avatar Sep 27 '25 16:09 sillvva

Just copying my reproduction from the duplicate issue above into this one in case it helps. The circumstances required to trigger the issue seemed very particular in my case.

Just wish I had found this issue before spending hours narrowing down the problem 😅


Reproduction: https://www.sveltelab.dev/j093ob9ce3xk50c?files=.%2Fsrc%2Froutes%2F%2Bpage.svelte%2C.%2Fsrc%2Froutes%2Fapi.remote.ts

Click the button and observe that the calls made double each time. Both the client enhance function and the server form function log when they are called.

The circumstances I've narrowed it down to so far are

  • Multiple instances of a form using .for() inside of an async each block calling a remote query
  • A top level await followed by a derived promise, which is then awaited in the template
  • The forms use .enhance()

dangodai avatar Nov 27 '25 04:11 dangodai

I can reproduce locally in my application with only an enhance() on the form. No need to use the for().

I don't think it's related to kit, but more to svelte itself.

I've never noticed it with version 5.41.x Testing with 5.45.2 yesterday, and noticed it right away. Removing the enhance makes it back to normal instantly.

bcharbonnier avatar Nov 28 '25 07:11 bcharbonnier

@bcharbonnier Did that ever resolve for you or did you just move away from using enhance?

Ran into this today with just enhance on a single form, meaning no .for, but it was not happening consistently. Closed the tab and reopened app in another, everything was fine for 10-15 minutes and then it starts popping up again. I feel like I've been seeing a lot of weird little ghosts like that since switching over to async svelte & remote queries/forms.

Update: Also noticed that, when this issue was happening, prelight-only validation also wasn't working - it was back to making a request to the server for validation that otherwise worked client-side.

kevlarr avatar Dec 15 '25 17:12 kevlarr