Forms & Validation¶
Building production-ready forms requires more than simple text synchronization. Developers must prevent input lag on fast typists, prevent DOM data caching, validate input fields using complex business logic, and render structured errors back to users.
To achieve this, reflex-django provides an advanced, state-driven validation framework that connects Reflex interactive inputs with Django's native field constraints and validation pipelines.
1. Choosing Your Input Style¶
reflex-django supports two input styles. Choose the style that best fits your design and performance requirements:
1. Flat Bindings (Real-time Sync) 2. Form Submit (Debounced Sync)
┌────────────────────────┐ ┌────────────────────────┐
│ value=State.title │ │ name="title" │
│ on_change=set_title │ │ │
│ (High reactivity) │ │ (Zero typing overhead) │
└────────────────────────┘ └────────────────────────┘
Style A: Flat Bindings (Reactivity-First)¶
Inputs are directly bound to state fields on the server. The state is updated in real time as the user types.
- Pros: Real-time styling, instant character counting, and immediate client-side feedback.
- Cons: Network round-trips for every keystroke (can be optimized using debounce settings).
rx.input(
value=ProductState.name,
on_change=ProductState.set_name,
placeholder="Enter product title..."
)
Style B: Form Submit (Performance-First)¶
Inputs are treated as standard un-bound HTML fields. The data is only transmitted to the server when the user clicks Submit.
- Pros: Zero typing latency, resilient over high-latency networks.
- Cons: No real-time server-side validation as the user types.
# Enable form submit processing in your state configuration
class ProductState(ModelState):
class Meta:
use_form_submit = True
# Bind the submission handler to your form
rx.form(
rx.vstack(
rx.input(name="name", placeholder="Product name..."),
rx.input(name="price", placeholder="0.00"),
rx.button("Save Item", type="submit"),
),
on_submit=ProductState.save_form, # Auto-generated submission handler
reset_on_submit=False,
key=ProductState.form_reset_key,
)
[!IMPORTANT] When using
use_form_submit, ensure that thename=attribute of eachrx.inputmatches the corresponding field name in your Django serializer.
2. Form Remounting & Caching Prevention¶
A common bug in reactive single-page applications (SPAs) is cached browser data. When you save or cancel a form, the server resets the variables. However, the browser may retain the user's typed text inside input fields.
To solve this, reflex-django uses an auto-incrementing integer: form_reset_key.
Form Actions (Save / Cancel / Load Edit)
│
▼
Server Resets State Variables
│
▼
Server Increments form_reset_key
│
▼
React Detects Key Change in the Client
│
▼
Remounts DOM Form Subtree (Discards Caches)
The Form Reset Lifecycle¶
The engine automatically manages the form_reset_key during different user actions:
| Action / Handler | Resets Python Variables? | Increments form_reset_key? |
Best For |
|---|---|---|---|
cancel_edit() |
Yes | Yes | Dismissing the editor. |
load(pk) |
Hydrates with row data | Yes | Entering edit mode. |
save() (Success) |
Yes (when reset_after_save=True) |
Yes | Finalizing record writes. |
reset_state_fields() |
Yes | Yes | Manually clearing all inputs. |
bump_form_reset_key() |
No | Yes | Clearing browser text manually without changing state data. |
To enforce this, always assign the key to your form container:
rx.form(
rx.vstack(
rx.input(value=State.title, on_change=State.set_title),
# All inputs inside this form are cleanly remounted when key changes
),
key=State.form_reset_key,
)
3. The Multi-Stage Validation Pipeline¶
Before data is saved to the database, reflex-django routes values through a structured validation pipeline:
Step 1: Normalization (clean_{field_name})¶
Define clean_<field> methods to normalize and validate individual fields. These methods should return the cleaned value or raise a serializers.ValidationError:
class PostState(ModelState):
model = BlogPost
fields = ["title", "slug"]
def clean_slug(self, value: str) -> str:
"""Normalize URL slug formats: strip whitespace, lowercase, and replace spaces."""
cleaned = value.strip().lower().replace(" ", "-")
if not cleaned:
raise serializers.ValidationError("Slug cannot be empty.")
return cleaned
Step 2: Cross-Field Validation (validate_state)¶
Override the async validate_state(self, ctx) hook to perform cross-field validation. Return a dictionary mapping field names to error messages:
class RegisterState(AppState):
password: str = ""
confirm_password: str = ""
async def validate_state(self, ctx) -> dict[str, str]:
errors = await super().validate_state(ctx)
if self.password != self.confirm_password:
errors["confirm_password"] = "Passwords do not match."
return errors
Step 3: Model-Level Constraints (run_model_validation)¶
Enable run_model_validation = True in your Meta class to automatically execute Django's native field constraints and .full_clean() validations:
class ProductState(ModelState):
model = Product
fields = ["name", "sku", "price"]
class Meta:
run_model_validation = True
structured_errors = True # Populates field-specific errors
This ensures that Django's built-in field validators (e.g. EmailValidator, unique constraints) are executed, and any errors are captured and returned to the UI.
4. Rendering Global & Field-Specific Errors¶
When validation fails, reflex-django updates your state's error variables so you can display them in the UI:
Global Error Buffer¶
Available as State.error (for ModelState) or State.{list_var}_error (for ModelCRUDView). It contains a general error summary, perfect for rendering a global banner:
rx.cond(
ProductState.error != "",
rx.callout(
ProductState.error,
color_scheme="red",
icon="alert_triangle",
),
)
Field-Specific Errors¶
When Meta.structured_errors = True is enabled, the engine populates a field_errors dictionary. You can bind these errors directly to their corresponding inputs:
rx.vstack(
rx.input(
value=ProductState.sku,
on_change=ProductState.set_sku,
placeholder="Enter SKU code...",
),
# Render error only if it exists for this field
rx.cond(
ProductState.field_errors.get("sku", "") != "",
rx.text(
ProductState.field_errors.get("sku", ""),
color="red",
size="1",
),
),
)
5. Complete Implementation: Form Validation UI¶
Here is a complete, production-ready form component showing our validation pipeline in action:
# blog/pages.py
import reflex as rx
from blog.states import BlogPostState
def styled_input_field(label: str, input_field: rx.Component, field_name: str) -> rx.Component:
"""Helper to render labeled inputs with real-time error messages."""
return rx.vstack(
rx.text(label, size="2", weight="medium", color="gray"),
input_field,
rx.cond(
BlogPostState.field_errors.get(field_name, "") != "",
rx.text(
BlogPostState.field_errors.get(field_name, ""),
color="red",
size="1",
weight="medium",
),
),
spacing="1",
width="100%",
)
def article_editor_form() -> rx.Component:
return rx.vstack(
rx.heading("Article Editor", size="6"),
rx.text("Draft stories, format slugs, and publish articles.", color="gray"),
rx.divider(),
# Form Container
rx.form(
rx.vstack(
styled_input_field(
"Article Title",
rx.input(
value=BlogPostState.title,
on_change=BlogPostState.set_title,
placeholder="Premium Headline...",
),
"title",
),
styled_input_field(
"URL Slug",
rx.input(
value=BlogPostState.slug,
on_change=BlogPostState.set_slug,
placeholder="slug-format-here",
),
"slug",
),
styled_input_field(
"Content Body",
rx.text_area(
value=BlogPostState.body,
on_change=BlogPostState.set_body,
placeholder="Write article...",
rows="6",
),
"body",
),
rx.hstack(
rx.button("Cancel", variant="soft", color_scheme="gray", on_click=BlogPostState.cancel_edit),
rx.spacer(),
rx.button("Save Draft", color_scheme="teal", on_click=BlogPostState.save),
width="100%",
),
spacing="4",
width="100%",
),
key=BlogPostState.form_reset_key,
width="100%",
),
spacing="4",
padding="2em",
border="1px solid var(--gray-4)",
border_radius="12px",
max_width="32em",
)
6. Authentication Form Validation Rules¶
reflex-django's session authentication routines also leverage structured validation parameters, which can be configured inside your project's settings.py file:
# django_project/settings.py
REFLEX_DJANGO_AUTH = {
# Password complexity rules
"PASSWORD_MIN_LENGTH": 8,
"PASSWORD_REQUIRE_DIGIT": True,
"PASSWORD_REQUIRE_SPECIAL": True,
# Registration options
"EMAIL_REQUIRED": True,
# Login parameter keys
"LOGIN_FIELDS": ("username", "email"),
}
The authentication mixins (like session_auth_mixin) automatically run these rules during user registration and login, mapping any failures to the global error buffer.
7. Common Form Pitfalls & Solutions¶
- Stale Text Inputs: If you do not assign
key=State.form_reset_keyto your form container, the client's browser may retain previously typed values after a save or cancel action. - Typing Keystroke Latency: If bound inputs feel sluggish or laggy, wrap your inputs in standard HTML fields using the
use_form_submitoption to batch transmissions. - Missing Field Declarations: If you are using
use_form_submit, ensure that your HTMLname="..."tags exactly match the fields defined on your Django serializer. Mismatched fields are silently ignored by the deserialization engine.
Navigation: ← reflex-django Mixins | Next: Command Line Interface →