Skip to content

feat(forms): graduate forms toward Stable — progressive-enhancement actions + optimistic updates (#139, #140) - #156

Merged
JosunLP merged 1 commit into
devfrom
feat/forms-stable-actions
Jun 28, 2026
Merged

feat(forms): graduate forms toward Stable — progressive-enhancement actions + optimistic updates (#139, #140)#156
JosunLP merged 1 commit into
devfrom
feat/forms-stable-actions

Conversation

@JosunLP

@JosunLP JosunLP commented Jun 28, 2026

Copy link
Copy Markdown
Collaborator

Collected PR for both open forms tickets, implemented on one branch.

Closes #139
Closes #140

#139 — Promote forms to Stable (freeze the 1.13 surface)

The forms surface expanded materially in 1.13.0 and has documented sharp edges. This freezes the surface for one minor cycle and settles those edges into guaranteed, tested contracts:

  • validationStrategy default reviewed + documented. The default stays 'manual' (the least-surprising choice for "validate on submit"). The contract is now explicit: handleSubmit() always runs the full validation pass, regardless of strategy — validationStrategy only gates automatic per-change/per-blur validation. Behaviour is unchanged; the surprise is removed by documentation.
  • SSR serialization boundary is now guaranteed. serializeFormState() deterministically drops functions, File / Blob / FileList, bigint, and symbol via an explicit replacer, instead of relying on incidental JSON.stringify behaviour. Re-attach blobs on the client after hydration.
  • createFieldArray() stable-key contract validated with clear errors. New optional getKey enforces present, unique keys on every structural mutation and throws a descriptive error naming the offending key (e.g. "requires stable, unique item keys, but getKey returned "a" for both index 0 and index 1"). Adds keys() / keyAt(index). Without getKey the array stays positional — no behaviour change.
  • Surface frozen + documented. New Stability section in the Forms guide with the exit-criteria checklist and frozen-surface reference; module header and stability matrices updated to "targeting Stable in 1.15.0".

#140 — Progressive-enhancement form actions + optimistic updates

The headline React-19-parity feature: forms that work before/without JS and reconcile optimistically.

import { formAction, useFormStatus, optimistic } from '@bquery/bquery/forms';

const submit = formAction('/todos', { method: 'POST', csrf: () => csrfToken });
const { pending } = useFormStatus(submit);
const list = optimistic(todos, (cur, draft) => [...cur, draft]);

submit.enhance(document.querySelector('form')!); // native POST without JS; fetch-enhanced with JS
  • formAction(target, options) → reactive pending / error / result / submitCount / submittedAt; enhance(form) sets the native action/method (+ optional hidden CSRF field) for the no-JS path, then intercepts submit for a fetch-based, optimistic-aware submit; programmatic submit(formData); reset(). target is an endpoint URL or a function. Non-OK responses throw FormActionError (carrying status / response). Composes with the validation pipeline and the server module's csrf() ([Feature]: First-party session, auth, and middleware primitives for server #132). Native forms only support GET/POST, so PUT/PATCH/DELETE degrade to a native POST (the enhanced fetch keeps the real verb).
  • useFormStatus(action) → read-only readonly() views of the action's signals, mirroring React 19's useFormStatus.
  • optimistic(base, reducer) → an optimistic-update primitive whose reactive value folds pending drafts over the base and reverts automatically. add(draft) → handle with remove(); run(draft, task) applies the overlay around an async task; pending / drafts reactive; clear().

Quality

  • 28 new tests across tests/forms-stable.test.ts (field-array key contract, SSR boundary, validationStrategy timing) and tests/forms-actions.test.ts (formAction function + string targets, CSRF, enhance/PE, optimistic composition, useFormStatus).
  • Full suite 2882 pass / 0 fail; tsc --noEmit, eslint, check-full-bundle (in sync), check-doc-exports (forms 46/46), and build:lib / build:types / build:umd all green.
  • New public exports (formAction, useFormStatus, optimistic, FormActionError, FieldArrayKeyFn + action/optimistic types) are wired into src/full.ts and documented in the guide.

🤖 Generated with Claude Code

…ctions + optimistic updates (#139, #140)

Implements both open `forms` tickets in one branch.

#139 — Promote `forms` to Stable (freeze the 1.13 surface):
- Document the `'manual'` validationStrategy default as a deliberate contract:
  handleSubmit() always runs the full validation pass; the strategy only gates
  automatic per-change/per-blur validation. (Behaviour unchanged; clarified.)
- Make the SSR serialization boundary a guaranteed contract: serializeFormState()
  now deterministically drops functions, File/Blob/FileList, bigint, and symbol
  via an explicit replacer instead of relying on incidental JSON.stringify.
- Validate the createFieldArray() stable-key contract: new optional `getKey`
  enforces present, unique keys on every structural mutation and throws a
  descriptive error naming the offending key; adds keys()/keyAt(). Positional
  (no-getKey) behaviour is unchanged.
- Freeze + document the public surface; add a Stability section with the
  exit-criteria checklist and frozen-surface reference; module header updated to
  "targeting Stable in 1.15.0".

#140 — Progressive-enhancement form actions + optimistic updates:
- formAction(target, options): binds a form to a server action (endpoint URL or
  function). enhance(form) sets the native action/method (+ optional hidden CSRF
  field) so it POSTs without JS, then intercepts submit for a fetch-based,
  optimistic-aware submit with reactive pending/error/result state. Non-OK
  responses throw FormActionError (status/response). Composes with server csrf().
- useFormStatus(action): read-only readonly() views of the action's signals
  (React 19 parity).
- optimistic(base, reducer): optimistic-update primitive whose reactive value
  folds pending drafts over the base; add()/run()/clear() + pending/drafts.

Tests: tests/forms-stable.test.ts, tests/forms-actions.test.ts (28 new cases).
Full suite 2882 pass / 0 fail; tsc, eslint, check-full-bundle, doc-exports
(forms 46/46), and lib/types/umd builds all green.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b0e8826a-13ef-41d1-a30d-cfc11d231fac

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/forms-stable-actions

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added docs Changes to the documentation tests Chenges to the tests forms Changes to the forms module labels Jun 28, 2026
@JosunLP
JosunLP merged commit 1f5f321 into dev Jun 28, 2026
9 checks passed
@JosunLP
JosunLP deleted the feat/forms-stable-actions branch June 28, 2026 21:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Changes to the documentation forms Changes to the forms module tests Chenges to the tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant