UI Form Patterns
Form handling patterns using TanStack Form and TanStack Query
DECISION STATUS: ACCEPTED
Overview
SuperImpress forms use a combination of TanStack Form for validation and TanStack Query for mutations, with deliberate patterns to prevent common UX issues like duplicate submissions and race conditions.
Spam Prevention Pattern
The Problem
Users can accidentally (or intentionally) submit forms multiple times by repeatedly clicking the submit button. This causes:
- Duplicate API requests
- Race conditions
- Inconsistent state
- Wasted server resources
- Poor user experience
The Solution
Disable form controls during submission by tracking the mutation's pending state.
Implementation (from frontend/src/features/auth/register/register.tsx):
<fieldset disabled={registerMutation.isPending}>
{/* All form inputs and submit button here */}
</fieldset>How It Works
- User submits form:
registerMutation.mutate(value)is called - Mutation starts:
registerMutation.isPendingbecomestrue - Fieldset disables: The
disabledattribute disables all form controls - User blocked: Cannot type, click submit, or interact with any field
- Mutation completes:
isPendingbecomesfalse, form re-enables
Why Fieldset Instead of Just the Button
Disabling only the submit button is insufficient:
- Users can still modify form data while request is in-flight
- Changed data creates inconsistent UI state
- Doesn't clearly communicate "form is processing"
Disabling the entire <fieldset> provides:
- Clear visual feedback (browser dims disabled controls)
- Prevents any form interaction during submission
- Single binding point - no need to disable each input individually
- Native HTML behavior - no custom state management
Visual Feedback
The submit button shows loading state:
<Button type="submit" aria-busy={registerMutation.isPending}>
{registerMutation.isPending ? 'Registering...' : 'Register'}
</Button>This provides:
- Text changes from "Register" to "Registering..."
aria-busyattribute for screen readers- Clear indication that work is happening
Form State Management
TanStack Form
Used for client-side validation and form state:
const form = useForm({
defaultValues: { email: '', password: '', confirmPassword: '' },
validators: { onSubmit: registerFormSchema },
onSubmit: async ({ value }) => {
registerMutation.mutate(value);
}
});Responsibilities:
- Field-level validation with Zod schemas
- Touch/dirty state tracking
- Error message display
- Form submission handling
TanStack Query Mutation
Used for server communication and async state:
const registerMutation = useMutation({
mutationFn: registerApi,
onSuccess: () => {
navigate('/login');
}
});Responsibilities:
- API request lifecycle (pending, success, error)
- Automatic retries and error handling
- Success/error callbacks
- Request deduplication
Why Both Libraries?
TanStack Form and TanStack Query have distinct, complementary roles:
- Form: Client-side validation, field state, UX
- Query: Server communication, async state, caching
Using both provides:
- Clear separation of concerns
- Best-in-class solutions for each domain
- Reactive state that works seamlessly together
Benefits
For Users
- Cannot accidentally submit forms multiple times
- Clear visual feedback during submission
- Consistent, predictable behavior
- Accessible loading states
For Developers
- Simple implementation (one
disabledbinding) - No custom debouncing or state management needed
- Leverages native HTML
<fieldset>behavior - Automatic with TanStack Query's
isPendingstate - Consistent pattern across all forms
For System
- Prevents duplicate API requests
- Reduces server load
- Avoids race conditions
- Cleaner error handling (one request at a time)
Accessibility
The pattern includes proper ARIA attributes:
aria-invalidon inputs with validation errorsaria-busyon submit button during submissionrole="alert"for error messagesaria-live="polite"for dynamic error announcements
Screen readers announce:
- Field validation errors as users type
- Form submission in progress
- Success or error after submission completes
Related Patterns
This decision is related to:
- UI Component Architecture - UI components used in forms
References
Code Examples
/frontend/src/features/auth/register/register.tsx- Registration form implementation/frontend/src/features/auth/login/login.tsx- Login form (same pattern)
Documentation
- TanStack Form - Form validation and state management
- TanStack Query - Async state and mutations
- MDN: fieldset - Native HTML fieldset element
- ARIA: busy state - Accessibility for loading states