04 — Form validation
Controlled inputs + derived validation state. No external state library required.
tsx
import { use } from 'flexium/core'
export function SignupForm() {
const [email, setEmail] = use('')
const [password, setPassword] = use('')
const [submitted, setSubmitted] = use(false)
// Derived validation — recomputed when email/password change
const [emailError] = use(() => {
if (!submitted && !email) return ''
if (!email) return 'Email is required'
if (!/^[^@]+@[^@]+\.[^@]+$/.test(email)) return 'Invalid email'
return ''
})
const [passwordError] = use(() => {
if (!submitted && !password) return ''
if (password.length < 8) return 'Password must be 8+ characters'
if (!/[0-9]/.test(password)) return 'Password must include a number'
return ''
})
const [isValid] = use(() => email && password && !emailError && !passwordError)
function submit(e: Event) {
e.preventDefault()
setSubmitted(true)
if (!isValid) return
// POST to your API…
console.log('submit', { email, password })
}
return (
<form onsubmit={submit}>
<label>
Email
<input type="email" value={email} oninput={(e: any) => setEmail(e.target.value)} />
{emailError && <span style="color:red">{emailError}</span>}
</label>
<label>
Password
<input type="password" value={password} oninput={(e: any) => setPassword(e.target.value)} />
{passwordError && <span style="color:red">{passwordError}</span>}
</label>
<button type="submit" disabled={submitted && !isValid}>Sign up</button>
</form>
)
}Why use(() => ...) for validation
The functional form use(() => expression) creates a derived signal: it re-evaluates whenever any signal it reads changes, and caches the result until then. Phase 1's lazy memoization means unchanged inputs cost zero compute.
Equivalent in shape to React's useMemo, but with fine-grained tracking:
emailErroronly re-evaluates whenemailorsubmittedchanges.passwordErroronly re-evaluates whenpasswordorsubmittedchanges.isValidre-evaluates when any of its 4 dependencies change.- The
<span>{emailError}</span>text node only updates whenemailErrorchanges.
Touched-only validation
Showing errors only after submit is opinionated. To show errors after the user blurs the field:
tsx
const [emailTouched, setEmailTouched] = use(false)
<input
type="email"
value={email}
oninput={(e: any) => setEmail(e.target.value)}
onblur={() => setEmailTouched(true)}
/>
{(submitted || emailTouched) && emailError && <span>{emailError}</span>}API surface used
use(value)— signaluse(() => expression)— derived signal (lazy + cached)<form onsubmit={...}>, native input events
Next
→ Routing — Routes, Link, useRouter.