Skip to content

Support TypedDict form data in on_submit#6301

Open
GautamBytes wants to merge 9 commits intoreflex-dev:mainfrom
GautamBytes:feat/support-typeddict-forms
Open

Support TypedDict form data in on_submit#6301
GautamBytes wants to merge 9 commits intoreflex-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

✅ 9 untouched benchmarks


Comparing GautamBytes:feat/support-typeddict-forms (80be5f4) with main (4e7b239)

Open in CodSpeed

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 8, 2026

Greptile Summary

This PR adds TypedDict support for on_submit form data handlers in Reflex forms. Two complementary changes enable this:

  1. Type-checking relaxation (event/__init__.py): A new helper _is_mapping_style_event_arg_compatible_with_typed_dict lets any Mapping-typed event arg satisfy a TypedDict-annotated callback parameter, bypassing the stricter typehint_issubclass check. A new on_submit_mapping_event overload on Form.on_submit exposes a generic Mapping type for the submission payload.
  2. Compile-time field validation (forms.py): _validate_on_submit_typed_dict_fields is invoked at form construction time. It resolves required keys from the TypedDict annotation, collects static form field identifiers (both name props and id-backed refs), and raises EventHandlerValueError if any required keys are absent — unless dynamic Var identifiers are present, in which case validation is skipped.

Key changes:

  • Form.on_submit gains a on_submit_mapping_event overload to support generic Mapping payloads
  • _is_mapping_style_event_arg_compatible_with_typed_dict globally relaxes Mapping→TypedDict compatibility across all event handlers
  • BASE_STATE TypeVar is moved from the bottom TYPE_CHECKING block to the top one, leaving a now-redundant import at line ~2593
  • Comprehensive test suite covers acceptance, optional fields, extra fields, bound-arg resolution, missing-field errors, and dynamic-name bypass

Confidence Score: 5/5

Safe to merge — all remaining findings are non-blocking style and defensive-coding suggestions with no impact on correctness or existing behaviour

The implementation is well-structured and thoroughly tested. The four findings are all P2: a redundant dead-code import block, a narrow exception catch that could be broadened for defensive safety, a deliberate (though undocumented) global relaxation of Mapping→TypedDict compatibility, and a minor double traversal of form refs. None of these affect runtime correctness or existing users. The runtime payload shape is explicitly unchanged, backward compatibility is preserved, and the new validation only fires at construction time with a clear error message.

packages/reflex-base/src/reflex_base/event/init.py — redundant TYPE_CHECKING block at the bottom and the global scope of the Mapping relaxation are worth a second look before merge

Vulnerabilities

No security concerns identified. The new TypedDict validation runs at Python import/construction time using only type introspection (get_type_hints, is_typeddict, __required_keys__). No user-supplied data reaches the validation path, and the runtime payload shape is explicitly unchanged.

Important Files Changed

Filename Overview
packages/reflex-components-core/src/reflex_components_core/el/elements/forms.py Core change: adds TypedDict field validation at form construction time, new mapping submit event overload, and several helper functions for static field discovery — well-structured but has a subtle gap where get_type_hints only catches NameError
packages/reflex-base/src/reflex_base/event/init.py Adds _is_mapping_style_event_arg_compatible_with_typed_dict to relax type checks globally for Mapping→TypedDict compatibility; moves BASE_STATE TypeVar to top but leaves a now-redundant if TYPE_CHECKING import block at the bottom
tests/units/components/forms/test_form.py Comprehensive new test suite covering acceptance, optional fields, extra fields, bound-arg resolution, missing-field errors, and dynamic-name bypass — good coverage of the happy and error paths
pyi_hashes.json Hash update for the changed forms.py stub — routine maintenance change

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["Form.create"] --> B{on_submit in props?}
    B -- No --> C[Assign prevent_default]
    B -- Yes --> D[Build form component]
    C --> D
    D --> E[_validate_on_submit_typed_dict_fields]
    E --> F{on_submit is EventChain?}
    F -- No --> Z[Skip validation]
    F -- Yes --> G{Any non-EventSpec events?}
    G -- Yes --> Z
    G -- No --> H[_resolve_on_submit_typed_dict_contract]
    H --> I{form_data arg found?}
    I -- No --> Z
    I -- Yes --> J[get_type_hints on handler func]
    J --> K{annotation is TypedDict?}
    K -- No --> Z
    K -- Yes --> L[_get_static_form_field_keys]
    L --> M[Collect id-backed refs]
    M --> N[Scan children for static name props]
    N --> O{Any dynamic Var identifiers?}
    O -- Yes --> Z
    O -- No --> P{Missing required keys?}
    P -- No --> Z[Validation passes]
    P -- Yes --> Q[Raise EventHandlerValueError]
Loading

Reviews (1): Last reviewed commit: "Support TypedDict form submit data" | 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
Collaborator

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.
return _get_handler_name(event_spec.handler), annotation, required_fields


def _get_required_typed_dict_fields(typed_dict_type: type[Any]) -> frozenset[str]:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think we should check this here

required_keys = getattr(typed_dict_type, "__required_keys__", None)
    if required_keys is not None:
        return frozenset(required_keys)

return isinstance(value, Var) and value._js_expr == FORM_DATA._js_expr


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

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.

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

2 participants