Skip to content

fix(action): server-rendered and chained .with() urls - #646

Merged
ryansolid merged 3 commits into
nextfrom
fix/server-form-with-registered-action
Oct 1, 2026
Merged

ryansolid merged 3 commits into
nextfrom
fix/server-form-with-registered-action

Conversation

@ryansolid

Copy link
Copy Markdown
Member

Two fixes to how a .with() binding's url relates to the action it came from. Both only show up on paths that read the url instead of the in-memory binding — server-rendered forms, no-JavaScript posts — which server components made common: their markup reaches the client without the code that made the binding.

Found while rebuilding examples/todos-server in solidjs/solid (#3717), where every row's toggle is <form action={toggleTodo.with(t.id)}> rendered by a server component, and the client adds optimism with toggleTodo.onSubmit(...).

Server-rendered .with() forms run through their registered action

A .with() url (base?args=[...]) is registered on the client only when client code makes the same binding. Under SSR + hydration it always does; under a server component it never does. handleFormAction and submitServerForm looked the url up exactly, so the submission fell back to a synthesized generic action (server functions) or native submission (client actions). The server call still happened, but onSubmit / onSettled never ran and the submission was recorded under a different base, invisible to useSubmissions(action).

On a miss, the lookup now finds the base url and rebinds it to the rendered arguments with .with().

Chained .with() urls carry every bound argument

.with(a).with(b) rendered ?args=[b] while the in-memory binding was [a, b]. Direct calls were right; anything reading the url ran with arguments missing, and two chains ending in the same argument shared one url, so the later client registration replaced the earlier one. The url now encodes the whole binding — what the server's args handling already expects — and matches .with(a, b).

Public API changes

No new or removed surface. Behavior changes:

  • A submitted form whose action is a server-rendered .with() url of a registered action now runs that action's onSubmit and onSettled hooks with the bound arguments, and its submission is recorded under the action's base (visible to useSubmissions(action)). Previously it ran as a generic invocation with no hooks (server actions) or submitted natively (client actions).
  • action.with(a).with(b).url changes from …?args=[b] to …?args=[a,b].

Tests

  • generic server actions: a server-rendered .with() form on a server action, and on a client action, runs through the registered base action (hooks called with the bound argument, no fetch, submission under base). Both fail before the fix.
  • action: a chained .with url carries every bound argument, differs from a chain with a different first argument, is the registered instance, and equals the single-call form. Fails before the fix.

Full suite 472 passed, server suite 76 passed, tsc clean. Verified in the browser against the todos example: optimistic toggles, toggle-all and clear-completed run through the actions' hooks; before the fix none did.

ryansolid and others added 2 commits September 30, 2026 21:13
…ered action

A form rendered with `action.with(...args)` carries `base?args=[...]` as its
action url. That url is registered on the client only when client code makes
the same binding — true for SSR + hydration, where the component that renders
the form runs again, but not for a server component, whose markup arrives
without its code. The registry lookup in `handleFormAction` and
`submitServerForm` was exact, so the submission fell back to a synthesized
generic action (server functions) or native submission (client actions):
the server call still happened, but the base action's `onSubmit` and
`onSettled` hooks never ran and the submission was recorded under a
different base, so `useSubmissions(action)` never saw it.

On a miss, `findAction` now looks up the base url and rebinds it to the
rendered arguments with `.with()`, so the submission runs exactly as a
client-made binding would.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
`.with()` set `?args` to the current call's arguments only, so
`action.with(a).with(b)` rendered `?args=[b]` while the in-memory binding
was `[a, b]`. Direct client calls use the in-memory arguments and worked;
everything that reads the url did not — a server-rendered or no-JavaScript
submission ran with arguments missing, and two chains ending in the same
argument shared one url, so the later registration replaced the earlier
and its form submitted with the wrong binding.

The url now encodes the whole binding, which is what the server's `args`
handling expects, and `.with(a).with(b)` produces the same url as
`.with(a, b)`.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@changeset-bot

changeset-bot Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4897e98

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@solidjs/router Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@ryansolid
ryansolid merged commit 9ba0fca into next Oct 1, 2026
4 checks passed
@ryansolid
ryansolid deleted the fix/server-form-with-registered-action branch October 1, 2026 07:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant