fix(action): server-rendered and chained .with() urls - #646
Merged
Merged
Conversation
…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 detectedLatest commit: 4897e98 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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-serverin 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 withtoggleTodo.onSubmit(...).Server-rendered
.with()forms run through their registered actionA
.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.handleFormActionandsubmitServerFormlooked 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, butonSubmit/onSettlednever ran and the submission was recorded under a different base, invisible touseSubmissions(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'sargshandling already expects — and matches.with(a, b).Public API changes
No new or removed surface. Behavior changes:
.with()url of a registered action now runs that action'sonSubmitandonSettledhooks with the bound arguments, and its submission is recorded under the action'sbase(visible touseSubmissions(action)). Previously it ran as a generic invocation with no hooks (server actions) or submitted natively (client actions).action.with(a).with(b).urlchanges 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 underbase). Both fail before the fix.action: a chained.withurl 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,
tscclean. 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.