Skip to content

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:

  • emailError only re-evaluates when email or submitted changes.
  • passwordError only re-evaluates when password or submitted changes.
  • isValid re-evaluates when any of its 4 dependencies change.
  • The <span>{emailError}</span> text node only updates when emailError changes.

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) — signal
  • use(() => expression) — derived signal (lazy + cached)
  • <form onsubmit={...}>, native input events

Next

RoutingRoutes, Link, useRouter.

Released under the MIT License.