Skip to content

Support TypedDict form data in on_submit#6301

Open
GautamBytes wants to merge 13 commits into
reflex-dev:mainfrom
GautamBytes:feat/support-typeddict-forms
Open

Support TypedDict form data in on_submit#6301
GautamBytes wants to merge 13 commits into
reflex-dev:mainfrom
GautamBytes:feat/support-typeddict-forms

Conversation

@GautamBytes
Copy link
Copy Markdown
Contributor

All Submissions:

  • Have you followed the guidelines stated in CONTRIBUTING.md file?
  • Have you checked to ensure there aren't any other open Pull Requests for the desired changed?

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

New Feature Submission:

  • Does your submission pass the tests?
  • Have you linted your code locally prior to submission?

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?

Description

This PR adds TypedDict support for on_submit form data handlers while keeping existing dict[str, Any] and dict[str, str] handlers working as before.

What changed:

  • allows on_submit handlers annotated with a concrete TypedDict
  • validates required TypedDict keys against statically knowable form fields at form construction time
  • includes both literal name fields and existing id-backed form refs in the validation set
  • skips strict validation when field identifiers are dynamic or the submit chain is opaque, to avoid false positives
  • continues to allow extra form fields
  • keeps the runtime payload shape unchanged

This improves:

  • IDE autocomplete and handler authoring
  • static typing for form payload keys
  • compile-time feedback when a form is missing required fields expected by the handler

closes #6264

Signed-off-by: Gautam Manchandani <gautammanch@Gautams-MacBook-Air.local>
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Apr 8, 2026

Merging this PR will not alter performance

✅ 24 untouched benchmarks
⏩ 2 skipped benchmarks1


Comparing GautamBytes:feat/support-typeddict-forms (298036a) with main (b76fdc0)

Open in CodSpeed

Footnotes

  1. 2 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 8, 2026

Greptile Summary

This PR adds compile-time TypedDict support for on_submit form handlers. When a handler's form-data parameter is annotated with a concrete TypedDict, Reflex now statically validates that every required key is present in the form at construction time, emitting a focused EventHandlerValueError if any required field is missing.

  • forms.py gains _validate_on_submit_typed_dict_fields, which walks the event chain, reflects on handler type hints, resolves required TypedDict keys (with Python 3.10/3.11 cross-version handling for NotRequired), and compares them against the statically discoverable form fields. Validation is intentionally skipped for dynamic field identifiers, forms with an id, and opaque event-chain entries.
  • A new _is_form_control: ClassVar[bool] = False flag is added to Component and set to True on all standard HTML and Radix form controls so the field-discovery traversal knows which children contribute named form data.
  • Existing dict[str, Any] / dict[str, str] handlers are unaffected; the TypedDict relaxation in the type-compatibility check is scoped exclusively to on_submit triggers.

Confidence Score: 4/5

Safe to merge after fixing the func.__qualname__ vs event.handler.fn.__qualname__ inconsistency in _validate_on_submit_typed_dict_fields.

The static validation logic is well-designed and the tests are thorough. The one concrete defect is in _validate_on_submit_typed_dict_fields: after correctly unwrapping a partial handler into func, the code reads event.handler.fn.__qualname__ (the partial object) instead of func.__qualname__ (the actual function). Since functools.partial objects don't carry __qualname__, this raises AttributeError for any partial-wrapped handler with a TypedDict annotation, replacing the intended validation error with an opaque crash.

packages/reflex-components-core/src/reflex_components_core/el/elements/forms.py — specifically the typed_dict_contracts.append(...) call in _validate_on_submit_typed_dict_fields.

Important Files Changed

Filename Overview
packages/reflex-components-core/src/reflex_components_core/el/elements/forms.py Core of the TypedDict feature: adds static validation of on_submit handlers against form fields. Contains a bug where event.handler.fn.__qualname__ raises AttributeError for partial-wrapped handlers instead of using func.__qualname__.
packages/reflex-base/src/reflex_base/event/init.py Adds FORM_SUBMIT_MAPPING TypeVar and _is_on_submit_mapping_event_arg_compatible_with_typed_dict that gates the TypedDict relaxation to on_submit triggers only. Also moves BASE_STATE TypeVar under TYPE_CHECKING and converts affected annotations to string literals.
packages/reflex-base/src/reflex_base/components/component.py Minimal change: adds _is_form_control: ClassVar[bool] = False as a new component flag used by form validation traversal.
tests/units/components/forms/test_form.py Adds comprehensive unit tests covering TypedDict acceptance, optional fields, inherited TypedDicts, dynamic identifiers, and missing-field error messages.
tests/integration/test_typeddict_form_submit.py New integration test exercising TypedDict form submission end-to-end with both HTML and Radix form components and an inherited TypedDict variant.
packages/reflex-base/src/reflex_base/utils/pyi_generator.py Adds FORM_SUBMIT_MAPPING to the default imports for generated .pyi stubs so the new TypeVar is available in type-check stubs.
packages/reflex-components-radix/src/reflex_components_radix/themes/components/checkbox.py Adds _is_form_control = True so the Radix Checkbox is recognised as a named form field during static TypedDict validation.
packages/reflex-components-radix/src/reflex_components_radix/primitives/slider.py Adds _is_form_control = True to SliderRoot so the Radix primitive slider is included in form-field discovery.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["Form.create(*children, on_submit=handler)"] --> B["_validate_on_submit_typed_dict_fields()"]
    B --> C{"on_submit is EventChain?"}
    C -- No --> Z["Skip validation"]
    C -- Yes --> D["Iterate events in chain"]
    D --> E{"event is EventSpec?"}
    E -- No --> Z
    E -- Yes --> F{"args include FORM_DATA?"}
    F -- No --> D
    F -- Yes --> G["Extract handler.fn (unwrap partial)"]
    G --> H["get_type_hints(func)"]
    H --> I{"annotation is TypedDict?"}
    I -- No --> D
    I -- Yes --> J["Collect required_fields via _get_required_typed_dict_fields()"]
    J --> D
    D --> K{"typed_dict_contracts empty?"}
    K -- Yes --> Z
    K -- No --> L{"form has id?"}
    L -- Yes --> Z
    L -- No --> M["_get_static_form_field_keys()"]
    M --> N["Collect form refs + _is_form_control name/id props"]
    N --> O{"All required fields present?"}
    O -- Yes --> P["✓ Form created OK"]
    O -- No --> Q{"has_dynamic_identifiers?"}
    Q -- Yes --> P
    Q -- No --> R["✗ raise EventHandlerValueError"]
Loading

Reviews (2): Last reviewed commit: "update pyi_files" | Re-trigger Greptile

Comment thread packages/reflex-base/src/reflex_base/event/__init__.py
Comment thread packages/reflex-components-core/src/reflex_components_core/el/elements/forms.py Outdated
Comment thread packages/reflex-base/src/reflex_base/event/__init__.py Outdated
@FarhanAliRaza
Copy link
Copy Markdown
Contributor

Can you please add these tests as well and fix the issue?

def test_on_submit_accepts_typed_dict_with_inherited_optional_fields():
    """Inherited optional TypedDict keys should remain optional."""

    class BaseSignupData(TypedDict, total=False):
        nickname: str

    class SignupData(BaseSignupData):
        email: str

    class SignupState(rx.State):
        @rx.event
        def on_submit(self, form_data: SignupData):
            pass

    form = HTMLForm.create(
        Input.create(name="email"),
        on_submit=SignupState.on_submit,
    )

    assert isinstance(form.event_triggers["on_submit"], EventChain)
def test_on_submit_accepts_controls_associated_via_form_attribute():
    """Controls associated via the HTML form attribute should not fail validation."""

    class SignupData(TypedDict):
        email: str

    class SignupState(rx.State):
        @rx.event
        def on_submit(self, form_data: SignupData):
            pass

    form = HTMLForm.create(
        id="signup",
        on_submit=SignupState.on_submit,
    )
    Input.create(name="email", form="signup")

    assert isinstance(form.event_triggers["on_submit"], EventChain)

FarhanAliRaza and others added 7 commits April 16, 2026 16:13
Use __required_keys__ for inherited TypedDict optional fields, skip
validation for forms with id (HTML form attribute), replace stringly-typed
control detection with _is_form_control marker, use concrete Mapping type
in event spec, narrow exception handling, and add integration tests.
On 3.10, typing.TypedDict ignores typing_extensions.NotRequired when
populating __required_keys__. Fall back to annotation inspection to
subtract NotRequired fields on older Python versions.
Unit tests now pair happy paths with failing counterparts to prove
validation is active. Integration test carries input/expected data
per variant instead of fragile runtime app_source detection.
FarhanAliRaza
FarhanAliRaza previously approved these changes Apr 16, 2026
return isinstance(value, Var) and value._js_expr == FORM_DATA._js_expr


def _get_handler_name(handler: EventHandler) -> str:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think this can be inlined. As I understand it is called once.

Copy link
Copy Markdown
Collaborator

@masenf masenf left a comment

Choose a reason for hiding this comment

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

update from main.

this is a nice feature. i think the new iter_form_fields functionality could be used to replace _get_all_refs in the Form implementation which is the only place where that is really used. We could get rid of that relatively shaky method.

will also have to make sure this continues to work when the form's children are memoized components (have event handlers or other state associated with them). update the integration test to make sure at least one of the fields depends on state and thus will get auto-memoized.

@masenf masenf requested a review from a team as a code owner May 10, 2026 20:08
@masenf
Copy link
Copy Markdown
Collaborator

masenf commented May 11, 2026

@greptile-apps re-review this pr

masenf and others added 2 commits May 11, 2026 11:19
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
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.

on_submit should support TypedDict for form data

3 participants