Multi-value form data and TypedDict coercion; deprecate id-based form fields

Merged
MA
2d ago
+2561-78

A form submitted every value of a repeated name (a checkbox group, hidden inputs that share a name, a two-thumb slider), but Object.fromEntries kept only the last one, so handlers never saw the rest. The form's ordered entries now reach the backend, and the handler's annotation decides what it receives:

  • rx.form.FormData[str, str]: a read-only mapping that keeps every value. form_data["x"] returns the last value; getlist("x"), or its alias getAll, returns all of them.
  • A TypedDict:
    • list[str] fields collect every value of their name. A list field range also collects the range[] values that a two-thumb slider submits.
    • bool fields are False when nothing was submitted or the value is false-like ("false", "off", "no", "n", "0", empty).
    • X | None fields are None when nothing was submitted; unsubmitted NotRequired fields are left out.
    • Required/NotRequired are now resolved correctly under postponed annotations, and generic TypedDicts are specialized.
  • dict or no annotation: unchanged, the last value of each name, as on main.

This PR is non-breaking. The removal of id-based form fields is split into the stacked reflex-dev/reflex#7540 for 0.11.0:

  • A control with an id still adds a field read from its ref, and that value still replaces the value submitted under the same name. A name the form submits more than once keeps all its values.
  • Compiling a form whose handler takes the form data now emits a deprecation warning (deprecated in 0.10.1, removed in 0.11.0) for each value-bearing control with a static id but no name.

How it works:

  • getFormData in state.js collects the FormData entries, applies the id-ref values, and returns a plain object for client-side use.
  • The entries are wrapped as {"__reflex_form_data__": [...]} just before the event is emitted.
  • reflex_base.utils.form.transform_form_data builds the argument from its annotation. A FormData passed on to another handler is converted the same way.
  • rx.form is now stubbed as a class, so rx.form.FormData[...] works in type annotations.

Fixes #7342

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • This change requires a documentation update

Changes To Core Features:

  • Have you added an explanation of what your changes do and why you'd like us to include them?
  • Have you written new tests for your core changes, as applicable?
  • Have you successfully ran tests with your changes locally?

🤖 Generated with Claude Code

https://claude.ai/code/session_01TmumE4Q7EpbbSpvEAAoqZ6