React 表單處理 (Forms)
表單(form)元件透過和使用者互動的過程會產生資料狀態的變化。本篇將介紹 React 中處理表單的方式,包括傳統的受控元件方式,以及現代的 Actions 方式。
受控元件 (Controlled Components)
受控元件是指由 React 的 state 來控制表單元素值的元件。表單元素的值和 state 同步,每次輸入都會更新 state。
import { useState } from 'react'
function ControlledForm() {
const [name, setName] = useState('')
function handleSubmit(event) {
event.preventDefault()
console.log('提交的名字:', name)
}
return (
<form onSubmit={handleSubmit}>
<input type="text" value={name} onChange={(e) => setName(e.target.value)} />
<button type="submit">送出</button>
</form>
)
}
特點:
- 使用
value設定表單元素的值 - 使用
onChange監聽變化並更新 state - 表單值完全由 React 控制
處理多個輸入欄位
當表單有多個欄位時,可以使用一個 state 物件來管理:
import { useState } from 'react'
function RegistrationForm() {
const [formData, setFormData] = useState({
username: '',
email: '',
password: '',
})
function handleChange(event) {
const { name, value } = event.target
setFormData((prev) => ({
...prev,
[name]: value,
}))
}
function handleSubmit(event) {
event.preventDefault()
console.log('表單資料:', formData)
}
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="username">帳號:</label>
<input
type="text"
id="username"
name="username"
value={formData.username}
onChange={handleChange}
/>
</div>
<div>
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
/>
</div>
<div>
<label htmlFor="password">密碼:</label>
<input
type="password"
id="password"
name="password"
value={formData.password}
onChange={handleChange}
/>
</div>
<button type="submit">註冊</button>
</form>
)
}
各種表單元素
<textarea>
在 React 中,<textarea> 使用 value 來設定內容:
<textarea value={message} onChange={(e) => setMessage(e.target.value)} />
<select>
使用 value 來設定選中的選項:
<select value={fruit} onChange={(e) => setFruit(e.target.value)}>
<option value="apple">蘋果</option>
<option value="banana">香蕉</option>
<option value="orange">橘子</option>
</select>
多選的情況:
<select
multiple
value={selectedFruits}
onChange={(e) => {
const values = Array.from(e.target.selectedOptions, (opt) => opt.value)
setSelectedFruits(values)
}}
>
<option value="apple">蘋果</option>
<option value="banana">香蕉</option>
<option value="orange">橘子</option>
</select>
<input type="checkbox">
使用 checked 屬性:
<input type="checkbox" checked={isAgree} onChange={(e) => setIsAgree(e.target.checked)} />
<input type="radio">
function GenderSelect() {
const [gender, setGender] = useState('')
return (
<div>
<label>
<input
type="radio"
name="gender"
value="male"
checked={gender === 'male'}
onChange={(e) => setGender(e.target.value)}
/>
男
</label>
<label>
<input
type="radio"
name="gender"
value="female"
checked={gender === 'female'}
onChange={(e) => setGender(e.target.value)}
/>
女
</label>
</div>
)
}
非受控元件 (Uncontrolled Components)
非受控元件讓表單元素自己管理狀態,透過 ref 來取得值:
import { useRef } from 'react'
function UncontrolledForm() {
const inputRef = useRef(null)
function handleSubmit(event) {
event.preventDefault()
console.log('輸入的值:', inputRef.current.value)
}
return (
<form onSubmit={handleSubmit}>
<input type="text" ref={inputRef} defaultValue="預設值" />
<button type="submit">送出</button>
</form>
)
}
非受控元件使用
defaultValue 而不是 value 來設定初始值。<input type="file">
檔案輸入是特殊的非受控元件,因為它的值是唯讀的:
function FileUpload() {
const fileRef = useRef(null)
function handleSubmit(event) {
event.preventDefault()
const file = fileRef.current.files[0]
console.log('選擇的檔案:', file.name)
}
return (
<form onSubmit={handleSubmit}>
<input type="file" ref={fileRef} />
<button type="submit">上傳</button>
</form>
)
}
Form Actions(現代方式)
React 提供了一種更簡潔的表單處理方式:直接在 <form> 上使用 action 屬性。
基本用法
function SimpleForm() {
async function handleSubmit(formData) {
const name = formData.get('name')
const email = formData.get('email')
console.log('提交:', { name, email })
}
return (
<form action={handleSubmit}>
<input type="text" name="name" placeholder="姓名" />
<input type="email" name="email" placeholder="Email" />
<button type="submit">送出</button>
</form>
)
}
使用 action 的好處:
- 不需要
event.preventDefault() - 自動收集表單資料為
FormData物件 - 可以直接使用 async 函式
useActionState
useActionState 是用來追蹤表單 action 狀態的 Hook:
import { useActionState } from 'react'
function LoginForm() {
async function login(previousState, formData) {
const email = formData.get('email')
const password = formData.get('password')
// 模擬 API 呼叫
await new Promise((resolve) => setTimeout(resolve, 1000))
if (email === 'test@example.com' && password === '123456') {
return { success: true, message: '登入成功!' }
}
return { success: false, message: '帳號或密碼錯誤' }
}
const [state, formAction, isPending] = useActionState(login, null)
return (
<form action={formAction}>
<div>
<input type="email" name="email" placeholder="Email" required />
</div>
<div>
<input type="password" name="password" placeholder="密碼" required />
</div>
<button type="submit" disabled={isPending}>
{isPending ? '登入中...' : '登入'}
</button>
{state && <p style={{ color: state.success ? 'green' : 'red' }}>{state.message}</p>}
</form>
)
}
useActionState 返回:
state:action 的返回值(初始為傳入的第二個參數)formAction:要傳給 form 的 action 函式isPending:是否正在執行中
useFormStatus
useFormStatus 可以在子元件中取得父表單的狀態:
import { useFormStatus } from 'react-dom'
function SubmitButton() {
const { pending } = useFormStatus()
return (
<button type="submit" disabled={pending}>
{pending ? '提交中...' : '送出'}
</button>
)
}
function ContactForm() {
async function handleSubmit(formData) {
// 處理表單...
await new Promise((resolve) => setTimeout(resolve, 2000))
}
return (
<form action={handleSubmit}>
<input type="text" name="name" placeholder="姓名" />
<input type="email" name="email" placeholder="Email" />
<textarea name="message" placeholder="訊息"></textarea>
{/* SubmitButton 可以自動知道表單的 pending 狀態 */}
<SubmitButton />
</form>
)
}
useFormStatus 必須在 <form> 的子元件中使用,不能在同一個元件中使用。表單驗證
使用 HTML5 驗證
<form action={handleSubmit}>
<input type="email" name="email" required />
<input type="text" name="name" minLength={2} maxLength={50} required />
<input type="number" name="age" min={0} max={150} />
<button type="submit">送出</button>
</form>
自訂驗證
import { useState } from 'react'
function ValidatedForm() {
const [errors, setErrors] = useState({})
function validate(formData) {
const newErrors = {}
const email = formData.get('email')
const password = formData.get('password')
if (!email.includes('@')) {
newErrors.email = '請輸入有效的 Email'
}
if (password.length < 8) {
newErrors.password = '密碼至少需要 8 個字元'
}
return newErrors
}
async function handleSubmit(formData) {
const validationErrors = validate(formData)
if (Object.keys(validationErrors).length > 0) {
setErrors(validationErrors)
return
}
setErrors({})
// 處理表單提交...
}
return (
<form action={handleSubmit}>
<div>
<input type="email" name="email" placeholder="Email" />
{errors.email && <span className="error">{errors.email}</span>}
</div>
<div>
<input type="password" name="password" placeholder="密碼" />
{errors.password && <span className="error">{errors.password}</span>}
</div>
<button type="submit">送出</button>
</form>
)
}
完整範例:會員註冊表單
import { useActionState } from 'react'
import { useFormStatus } from 'react-dom'
function SubmitButton() {
const { pending } = useFormStatus()
return (
<button type="submit" disabled={pending}>
{pending ? '註冊中...' : '註冊'}
</button>
)
}
function RegistrationForm() {
async function register(prevState, formData) {
const data = {
username: formData.get('username'),
email: formData.get('email'),
password: formData.get('password'),
confirmPassword: formData.get('confirmPassword'),
}
// 驗證
if (data.password !== data.confirmPassword) {
return { success: false, error: '密碼不一致' }
}
if (data.password.length < 8) {
return { success: false, error: '密碼至少需要 8 個字元' }
}
// 模擬 API 呼叫
await new Promise((resolve) => setTimeout(resolve, 1500))
// 模擬成功
return { success: true, message: '註冊成功!請查看 Email 進行驗證。' }
}
const [state, formAction] = useActionState(register, null)
return (
<form action={formAction} className="registration-form">
<h2>會員註冊</h2>
<div className="form-group">
<label htmlFor="username">帳號</label>
<input type="text" id="username" name="username" required minLength={3} maxLength={20} />
</div>
<div className="form-group">
<label htmlFor="email">Email</label>
<input type="email" id="email" name="email" required />
</div>
<div className="form-group">
<label htmlFor="password">密碼</label>
<input type="password" id="password" name="password" required minLength={8} />
</div>
<div className="form-group">
<label htmlFor="confirmPassword">確認密碼</label>
<input type="password" id="confirmPassword" name="confirmPassword" required />
</div>
{state?.error && <p className="error-message">{state.error}</p>}
{state?.success && <p className="success-message">{state.message}</p>}
<SubmitButton />
</form>
)
}