Skip to content

fix(sink): replace date input with calendar picker in next-form to fix useActionState serialization crash#10215

Open
Lukas1098 wants to merge 2 commits intoshadcn-ui:mainfrom
Lukas1098:form_with_useactionstate
Open

fix(sink): replace date input with calendar picker in next-form to fix useActionState serialization crash#10215
Lukas1098 wants to merge 2 commits intoshadcn-ui:mainfrom
Lukas1098:form_with_useactionstate

Conversation

@Lukas1098
Copy link
Copy Markdown

What

Fixes #10214

Replaces the native <Input type="date"> in the /sink/next-form example with a Calendar + Popover picker using local React state.

Why

The original implementation crashed on submit because formState.values.startDate is returned as a Date object from the server action. After useActionState serializes and deserializes the return value across the server/client boundary, startDate arrives on the client as a string. Calling .toISOString() on it in defaultValue throws a client-side exception.

Changes

  • example-form.tsx: replaced <Input type="date"> with Calendar + Popover picker and a hidden input to submit the value
  • schema.ts: fixed teamSize max validation from 10 to 50 to match the input constraint

Copilot AI review requested due to automatic review settings March 28, 2026 15:27
@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Mar 28, 2026

@Lukas1098 is attempting to deploy a commit to the shadcn-pro Team on Vercel.

A member of the Team first needs to authorize it.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates the /sink/next-form example to avoid a useActionState Date serialization crash by moving start-date selection to a client-side calendar picker, and aligns server-side validation with the UI constraint for team size.

Changes:

  • Replace the native type="date" input with a Calendar + Popover picker and hidden input submission.
  • Update teamSize Zod validation max from 10 to 50 to match the input’s max="50" constraint.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
apps/v4/app/(internal)/sink/(pages)/next-form/example-form.tsx Swaps date input to a calendar popover picker and submits via a hidden input.
apps/v4/app/(internal)/sink/(pages)/schema.ts Adjusts teamSize validation to allow up to 50.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

<input type="hidden" name="startDate" value={date ? date.toISOString() : ""} />
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" className="w-full justify-start" disabled={pending}>
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Popover trigger uses the shared Button component without an explicit type. Since Button renders a plain <button> and doesn’t set a default type, this will default to type="submit" inside the form, so clicking the date picker trigger can submit the form unexpectedly. Set type="button" on this trigger button.

Suggested change
<Button variant="outline" className="w-full justify-start" disabled={pending}>
<Button type="button" variant="outline" className="w-full justify-start" disabled={pending}>

Copilot uses AI. Check for mistakes.
<FieldDescription>
Choose when your subscription should start
</FieldDescription>
<input type="hidden" name="startDate" value={date ? date.toISOString() : ""} />
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value={date.toISOString()} can shift the selected calendar day for users in time zones ahead of UTC (local midnight becomes the previous UTC day), which can cause server-side validation (e.g. “in the past”) to fail for a date the user picked. Prefer submitting a date-only string (e.g. yyyy-MM-dd) or otherwise normalizing the value so the server interprets the same calendar day the user selected.

Suggested change
<input type="hidden" name="startDate" value={date ? date.toISOString() : ""} />
<input type="hidden" name="startDate" value={date ? format(date, "yyyy-MM-dd") : ""} />

Copilot uses AI. Check for mistakes.
<input type="hidden" name="startDate" value={date ? date.toISOString() : ""} />
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" className="w-full justify-start" disabled={pending}>
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<FieldLabel htmlFor="startDate"> no longer points at a visible form control (the only startDate element is a hidden input without an id). This breaks label-to-control association for screen readers and click-to-focus behavior. Consider putting id="startDate" on the trigger button (and/or wiring up aria-invalid/description) so the label targets the interactive control.

Suggested change
<Button variant="outline" className="w-full justify-start" disabled={pending}>
<Button
id="startDate"
variant="outline"
className="w-full justify-start"
disabled={pending}
aria-invalid={!!formState.errors?.startDate?.length}
>

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@mahdirajaee mahdirajaee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid fix -- useActionState requires all form state to be serializable, and a Date object from <input type="date"> returning through the action response would indeed crash serialization. Replacing it with a hidden input carrying date.toISOString() string is the right approach.

A couple of things worth considering: the date state initializes to new Date() but is completely independent of formState.values.startDate, meaning the calendar will always open to "today" regardless of what the server-side form state holds. After a validation error round-trip, the calendar won't reflect the previously submitted value. It would be more robust to initialize from formState.values.startDate and keep them in sync.

Also, when date is undefined (user clears selection), the hidden input sends value="" -- but the schema still expects a valid date, so this will produce a confusing zod parse error rather than the existing "Please select a start date" message. The removed <FieldDescription> text ("Choose when your subscription should start") was useful UX context and probably shouldn't have been dropped.

The unrelated teamSize max bump from 10 to 50 in schema.ts should be called out in the PR description or split into its own commit.

@Lukas1098
Copy link
Copy Markdown
Author

Solid fix -- useActionState requires all form state to be serializable, and a Date object from <input type="date"> returning through the action response would indeed crash serialization. Replacing it with a hidden input carrying date.toISOString() string is the right approach.

A couple of things worth considering: the date state initializes to new Date() but is completely independent of formState.values.startDate, meaning the calendar will always open to "today" regardless of what the server-side form state holds. After a validation error round-trip, the calendar won't reflect the previously submitted value. It would be more robust to initialize from formState.values.startDate and keep them in sync.

Also, when date is undefined (user clears selection), the hidden input sends value="" -- but the schema still expects a valid date, so this will produce a confusing zod parse error rather than the existing "Please select a start date" message. The removed <FieldDescription> text ("Choose when your subscription should start") was useful UX context and probably shouldn't have been dropped.

The unrelated teamSize max bump from 10 to 50 in schema.ts should be called out in the PR description or split into its own commit.

Updated based on feedback:

  • date state now initializes from formState.values.startDate and syncs via useEffect after validation error round-trips, so the calendar reflects the previously submitted value
  • onSelect={(d) => d && setDate(d)} prevents date from becoming undefined, ensuring the hidden input always sends a valid ISO string
  • Restored the <FieldDescription> text

Regarding the teamSize change in schema.ts: this is an unrelated correction — the input already had max="50" but the schema was still validating max(10), causing valid submissions to fail. Happy to split it into a separate commit if preferred.

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.

[bug]: /sink/next-form crashes on form submit due to Date serialization across server/client boundary

3 participants