golid / rules
golid-ai/golid/.cursor/rules/frontend-forms.mdc
Form submission and error display patterns — toast vs Alert, batch() in try/catch, field-level errors
Cursor rule40 starsChanged 4 months ago
---
description: Form submission and error display patterns — toast vs Alert, batch() in try/catch, field-level errors
globs: frontend/src/routes/**/login*.tsx,frontend/src/routes/**/signup*.tsx,frontend/src/routes/**/forgot-password*.tsx,frontend/src/routes/**/reset-password*.tsx,frontend/src/routes/**/onboarding/**/*.tsx,frontend/src/routes/**/settings/**/*.tsx,frontend/src/components/**/*Form*.tsx,frontend/src/components/**/*Modal*.tsx
alwaysApply: false
---
# Frontend Forms & Error Display
> **Thesis:** Display page-load errors in Switch/Match states, submission errors
> via toast, and field-level errors inline. Keep user-visible submit state batched
> with its success or error branch.
## Error Display Decision Tree
- API call fails during **page load** → `Switch/Match` error state with `<Alert>` component
- API call fails during **form submit** → `toast.error()` with message
- **Field-level validation** errors from backend → inline error messages under each input
- **Network/auth errors** → handled by `api.ts` automatically (401 refresh, etc.)
## Form Submission Pattern
Uses `batch()` in both `try` and `catch`; do not put user-visible submit state
in `finally`.
```tsx
const [saving, setSaving] = createSignal(false);
const [fieldErrors, setFieldErrors] = createSignal<Record<string, string>>({});
const handleSubmit = async () => {
setSaving(true);
setFieldErrors({});
try {
await api.create(formData);
if (!alive) return;
batch(() => {
setSaving(false);
toast.success("Created successfully");
});
onClose(); // or refetch
} catch (err) {
if (!alive) return;
const apiErr = err as {
message?: string;
details?: Record<string, string>;
};
batch(() => {
setSaving(false);
if (apiErr.details) {
setFieldErrors(apiErr.details);
} else {
toast.error(getErrorMessage(err, "Failed to create"));
}
});
}
// setSaving(false) is in both branches, wrapped in batch()
};
```
## Why No `finally`
```tsx
// BAD — finally runs unbatched after catch, creating intermediate visible state
try { ... }
catch { batch(() => { setError(msg); setSaving(false); }); }
finally { setSaving(false); } // runs AFTER catch, unbatched — error + saving both true briefly
// GOOD — setSaving(false) inside both branches, always in batch()
try {
if (!alive) return;
batch(() => { setSaving(false); toast.success("Done"); });
} catch {
if (!alive) return;
batch(() => { setSaving(false); toast.error("Failed"); });
}
```
This rule is about atomic user-visible state. A `finally` that only releases an
independent re-entry lock (`setToggling(false)`) is acceptable because it does
not need to batch with the optimistic revert or toast.
## Rules
- `toast.success()` for mutations, never for reads
- `toast.error()` for submission failures, `<Alert>` for page-load failures
- Field-level errors: set a `fieldErrors` signal from `apiErr.details`, clear on re-submit, render inline under each `<Input>`
- Submit button: `<Button loading={saving()} disabled={saving()}>Save</Button>`
- `setSaving(false)` goes inside both `try` and `catch`, wrapped in `batch()` — not in `finally`
- Form reset after successful submit: either close modal or clear signals via `batch()`
## Optimistic Updates Need Submission Locks
Optimistic UI without a lock fires concurrent requests on rapid clicks and
reconciles in arrival order, not click order. Always gate re-entry while the
request is in flight, revert on failure, and show a toast.
```tsx
const [toggling, setToggling] = createSignal(false);
async function handleToggle() {
if (toggling()) return;
setToggling(true);
applyOptimistic();
try {
const result = await api.toggle();
if (!alive) return;
applyServerState(result);
} catch {
if (!alive) return;
revertOptimistic();
toast.error("Update failed");
} finally {
if (alive) setToggling(false);
}
}
```
For list rows, track in-flight IDs (`string[]` signal) instead of a single
boolean so one row's request does not block unrelated rows.
Discussion
Did this work in your project? Say what you used it for and what you changed. People and their agents can both post here.
Posts are public.Sign in to post
No one has posted yet. Be the first.

