- การทำ Form Validation ด้วย Formik, Yup และ Zod ใน React
- 2. ติดตั้ง Formik, Yup และ Zod
- 3. Form Validation ด้วย Formik + Yup
- 4. ตัวอย่าง Registration Form ด้วย Formik + Yup
- 5. การแสดง Error ด้วย touched และ errors
- 6. Confirm Password ด้วย Yup
- 7. Form Validation ด้วย Formik + Zod
- 8. เชื่อม Formik กับ Zod
- 9. ตัวอย่าง Registration Form ด้วย Formik + Zod
- 10. Confirm Password ด้วย Zod
- 11. ตรวจสอบ Password ที่ซับซ้อนขึ้น
- 12. Validate เมื่อใด
- 13. Validation Flow
- 14. Yup vs Zod
- 15. ควรเลือก Yup หรือ Zod
- 16. ตัวอย่างโครงสร้าง Project
- 17. Validation ไม่ควรมีเฉพาะ Frontend
- 18. สรุป
#การทำ Form Validation ด้วย Formik, Yup และ Zod ใน React
การสร้างฟอร์มใน React ไม่ได้มีเพียงการรับค่าจาก <input> แต่ยังต้องจัดการหลายเรื่องพร้อมกัน เช่น
- ค่าในฟอร์ม (
values) - สถานะว่าผู้ใช้เคยแตะช่องนั้นหรือยัง (
touched) - ข้อผิดพลาด (
errors) - การ Submit
- การตรวจสอบข้อมูลหรือ Validation
- การแสดงสถานะกำลังส่งข้อมูล (
isSubmitting)
Formik ช่วยจัดการวงจรชีวิตของฟอร์ม ส่วน Yup และ Zod ทำหน้าที่กำหนดกฎตรวจสอบข้อมูลผ่าน Schema
แนวคิดโดยรวมคือ
User Input
|
v
Formik
|
+----------+
| |
v v
Yup Zod
| |
+----+-----+
|
v
Validation Result
|
+----+----+
| |
Valid Invalid
| |
Submit errors
#1. Formik คืออะไร
Formik เป็นไลบรารีสำหรับจัดการฟอร์มใน React โดยช่วยลด Boilerplate ที่เกิดจากการเขียน useState จำนวนมาก
Formik ดูแลข้อมูลสำคัญ เช่น
values
errors
touched
isSubmitting
handleChange
handleBlur
handleSubmit
ตัวอย่างโครงสร้างพื้นฐาน
import { Formik, Form, Field, ErrorMessage } from "formik";
export default function ExampleForm() {
return (
<Formik
initialValues={{
name: "",
email: "",
}}
onSubmit={(values) => {
console.log(values);
}}
>
<Form>
<Field name="name" />
<ErrorMessage name="name" component="div" />
<Field name="email" type="email" />
<ErrorMessage name="email" component="div" />
<button type="submit">Submit</button>
</Form>
</Formik>
);
}
Formik สามารถทำ Validation ได้หลายวิธี เช่น
- เขียน
validate()เอง - Field-level validation
- ใช้ Schema Validation
- ใช้ Yup ผ่าน
validationSchema
Formik มีการรองรับ Yup โดยตรงผ่าน validationSchema
#2. ติดตั้ง Formik, Yup และ Zod
หากต้องการทดลองทั้งสองแนวทาง
npm install formik yup zod
หรือใช้ pnpm
pnpm add formik yup zod
หรือ Yarn
yarn add formik yup zod
#3. Form Validation ด้วย Formik + Yup
Formik รองรับ Yup โดยตรง ทำให้เป็นแนวทางที่เขียนง่ายและอ่านง่ายมาก
#สร้าง Yup Schema
import * as Yup from "yup";
const validationSchema = Yup.object({
name: Yup.string()
.min(3, "ชื่อต้องมีอย่างน้อย 3 ตัวอักษร")
.required("กรุณากรอกชื่อ"),
email: Yup.string()
.email("รูปแบบอีเมลไม่ถูกต้อง")
.required("กรุณากรอกอีเมล"),
password: Yup.string()
.min(8, "รหัสผ่านต้องมีอย่างน้อย 8 ตัวอักษร")
.required("กรุณากรอกรหัสผ่าน"),
});
Schema จะกำหนดกฎของข้อมูลแต่ละ Field
| Field | Validation |
|---|---|
name |
ต้องกรอก และยาวอย่างน้อย 3 ตัวอักษร |
email |
ต้องกรอก และเป็นรูปแบบ Email |
password |
ต้องกรอก และยาวอย่างน้อย 8 ตัวอักษร |
#4. ตัวอย่าง Registration Form ด้วย Formik + Yup
import { Formik, Form, Field, ErrorMessage } from "formik";
import * as Yup from "yup";
interface FormValues {
name: string;
email: string;
password: string;
}
const initialValues: FormValues = {
name: "",
email: "",
password: "",
};
const validationSchema = Yup.object({
name: Yup.string()
.min(3, "ชื่อต้องมีอย่างน้อย 3 ตัวอักษร")
.required("กรุณากรอกชื่อ"),
email: Yup.string()
.email("รูปแบบอีเมลไม่ถูกต้อง")
.required("กรุณากรอกอีเมล"),
password: Yup.string()
.min(8, "รหัสผ่านต้องมีอย่างน้อย 8 ตัวอักษร")
.required("กรุณากรอกรหัสผ่าน"),
});
export default function RegisterForm() {
return (
<Formik<FormValues>
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={(values, { setSubmitting }) => {
console.log(values);
setSubmitting(false);
}}
>
{({ isSubmitting }) => (
<Form>
<div>
<label htmlFor="name">Name</label>
<Field
id="name"
name="name"
placeholder="ชื่อ"
/>
<ErrorMessage
name="name"
component="div"
/>
</div>
<div>
<label htmlFor="email">Email</label>
<Field
id="email"
name="email"
type="email"
placeholder="example@email.com"
/>
<ErrorMessage
name="email"
component="div"
/>
</div>
<div>
<label htmlFor="password">Password</label>
<Field
id="password"
name="password"
type="password"
/>
<ErrorMessage
name="password"
component="div"
/>
</div>
<button
type="submit"
disabled={isSubmitting}
>
Register
</button>
</Form>
)}
</Formik>
);
}
จุดสำคัญคือ
validationSchema={validationSchema}
Formik จะเรียก Yup เพื่อตรวจสอบข้อมูล และนำ Error ที่ได้ไปเก็บใน errors ให้อัตโนมัติ
#5. การแสดง Error ด้วย touched และ errors
ในงานจริง เรามักไม่ต้องการแสดง Error ตั้งแต่ผู้ใช้ยังไม่ได้แตะช่อง Input
ตัวอย่าง
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={handleSubmit}
>
{({ errors, touched }) => (
<Form>
<Field name="email" />
{touched.email && errors.email && (
<div>{errors.email}</div>
)}
<button type="submit">
Submit
</button>
</Form>
)}
</Formik>
เงื่อนไข
touched.email && errors.email
หมายความว่า
แสดง Error เมื่อผู้ใช้เคยแตะช่อง Email แล้ว และ Email มีข้อผิดพลาด
#6. Confirm Password ด้วย Yup
กรณีสมัครสมาชิกมักต้องตรวจว่า password และ confirmPassword ตรงกัน
const validationSchema = Yup.object({
password: Yup.string()
.min(8, "รหัสผ่านต้องมีอย่างน้อย 8 ตัวอักษร")
.required("กรุณากรอกรหัสผ่าน"),
confirmPassword: Yup.string()
.oneOf(
[Yup.ref("password")],
"รหัสผ่านไม่ตรงกัน"
)
.required("กรุณายืนยันรหัสผ่าน"),
});
#7. Form Validation ด้วย Formik + Zod
Zod เป็น Schema Validation Library ที่ออกแบบมาโดยให้ TypeScript เป็นศูนย์กลาง
จุดเด่นสำคัญคือสามารถสร้าง Type จาก Schema ได้
import { z } from "zod";
const registerSchema = z.object({
name: z.string().min(3),
email: z.string().email(),
password: z.string().min(8),
});
type RegisterFormValues = z.infer<typeof registerSchema>;
ไม่ต้องเขียน Type ซ้ำ
interface RegisterFormValues {
name: string;
email: string;
password: string;
}
เพราะ Zod สามารถ Infer Type ได้จาก Schema
#8. เชื่อม Formik กับ Zod
Formik รองรับ Yup ผ่าน validationSchema โดยตรง ส่วน Zod สามารถเชื่อมผ่าน validate ของ Formik
ตัวอย่าง Helper
import {
FormikErrors,
setIn,
} from "formik";
import { z } from "zod";
function validateWithSchema<T>(
schema: z.ZodType<T>,
values: T
): FormikErrors<T> {
const result = schema.safeParse(values);
if (result.success) {
return {};
}
let errors: FormikErrors<T> = {};
for (const issue of result.error.issues) {
const path = issue.path.join(".");
errors = setIn(
errors,
path,
issue.message
);
}
return errors;
}
safeParse() จะไม่ Throw Error แต่จะคืนผลลัพธ์เป็น
{
success: true,
data: ...
}
หรือ
{
success: false,
error: ...
}
#9. ตัวอย่าง Registration Form ด้วย Formik + Zod
import {
Formik,
Form,
Field,
ErrorMessage,
FormikErrors,
setIn,
} from "formik";
import { z } from "zod";
const registerSchema = z.object({
name: z
.string()
.min(3, "ชื่อต้องมีอย่างน้อย 3 ตัวอักษร"),
email: z
.string()
.email("รูปแบบอีเมลไม่ถูกต้อง"),
password: z
.string()
.min(8, "รหัสผ่านต้องมีอย่างน้อย 8 ตัวอักษร"),
});
type FormValues = z.infer<typeof registerSchema>;
const initialValues: FormValues = {
name: "",
email: "",
password: "",
};
function validateForm(
values: FormValues
): FormikErrors<FormValues> {
const result = registerSchema.safeParse(values);
if (result.success) {
return {};
}
let errors: FormikErrors<FormValues> = {};
for (const issue of result.error.issues) {
errors = setIn(
errors,
issue.path.join("."),
issue.message
);
}
return errors;
}
export default function RegisterFormZod() {
return (
<Formik<FormValues>
initialValues={initialValues}
validate={validateForm}
onSubmit={(values) => {
console.log(values);
}}
>
{({ isSubmitting }) => (
<Form>
<div>
<label htmlFor="name">
Name
</label>
<Field
id="name"
name="name"
/>
<ErrorMessage
name="name"
component="div"
/>
</div>
<div>
<label htmlFor="email">
Email
</label>
<Field
id="email"
name="email"
type="email"
/>
<ErrorMessage
name="email"
component="div"
/>
</div>
<div>
<label htmlFor="password">
Password
</label>
<Field
id="password"
name="password"
type="password"
/>
<ErrorMessage
name="password"
component="div"
/>
</div>
<button
type="submit"
disabled={isSubmitting}
>
Register
</button>
</Form>
)}
</Formik>
);
}
#10. Confirm Password ด้วย Zod
เมื่อ Validation ต้องใช้ค่ามากกว่าหนึ่ง Field สามารถใช้ Refinement ได้
const registerSchema = z
.object({
password: z
.string()
.min(8, "รหัสผ่านต้องมีอย่างน้อย 8 ตัวอักษร"),
confirmPassword: z
.string()
.min(1, "กรุณายืนยันรหัสผ่าน"),
})
.refine(
(data) => data.password === data.confirmPassword,
{
message: "รหัสผ่านไม่ตรงกัน",
path: ["confirmPassword"],
}
);
Error จะถูกผูกกับ
confirmPassword
ทำให้ Formik สามารถแสดง Error ที่ Field นั้นได้
#11. ตรวจสอบ Password ที่ซับซ้อนขึ้น
ตัวอย่าง Password Policy
const passwordSchema = z
.string()
.min(8, "อย่างน้อย 8 ตัวอักษร")
.regex(
/[A-Z]/,
"ต้องมีตัวอักษรพิมพ์ใหญ่อย่างน้อย 1 ตัว"
)
.regex(
/[a-z]/,
"ต้องมีตัวอักษรพิมพ์เล็กอย่างน้อย 1 ตัว"
)
.regex(
/[0-9]/,
"ต้องมีตัวเลขอย่างน้อย 1 ตัว"
);
นำไปใช้
const schema = z.object({
password: passwordSchema,
});
#12. Validate เมื่อใด
Formik มีตัวเลือกสำคัญ
<Formik
validateOnChange={true}
validateOnBlur={true}
...
>
#validateOnChange
ตรวจทุกครั้งที่ Input เปลี่ยน
validateOnChange={true}
เหมาะกับฟอร์มที่ต้องการ Feedback แบบ Real-time
#validateOnBlur
ตรวจเมื่อผู้ใช้ออกจาก Input
validateOnBlur={true}
ช่วยลดการแจ้ง Error ขณะที่ผู้ใช้ยังพิมพ์ไม่เสร็จ
#13. Validation Flow
กระบวนการทำงานโดยทั่วไปคือ
User types input
|
v
Formik updates values
|
v
Validation runs
|
+-----------------+
| |
v v
Valid Invalid
| |
v v
errors = {} errors.email
| |
+--------+--------+
|
v
React re-render
|
v
Display feedback
#14. Yup vs Zod
| คุณสมบัติ | Yup | Zod |
|---|---|---|
| Formik Integration | ดีมาก / รองรับโดยตรง | ใช้ validate หรือ Adapter |
| Schema Validation | ✅ | ✅ |
| TypeScript | ดี | ดีมาก |
| Type Inference | ✅ | ✅ เด่นมาก |
| Async Validation | ✅ | ✅ |
| Transform Data | ✅ | ✅ |
| Learning Curve | ง่าย | ง่ายถึงปานกลาง |
| เหมาะกับ JavaScript | ✅ | ✅ |
| เหมาะกับ TypeScript | ✅ | ⭐⭐⭐ |
| Schema ใช้ร่วมกับ API/Server | ได้ | เหมาะมาก |
#15. ควรเลือก Yup หรือ Zod
#เลือก Formik + Yup เมื่อ
- ต้องการ Integration ที่ตรงไปตรงมากับ Formik
- ระบบมี Formik + Yup อยู่แล้ว
- ทีมคุ้นเคยกับ Yup
- เน้น Client-side Form Validation
- ต้องการโค้ด Validation ที่สั้นและอ่านง่าย
ตัวอย่าง
<Formik
validationSchema={validationSchema}
/>
#เลือก Formik + Zod เมื่อ
- Project ใช้ TypeScript เป็นหลัก
- ต้องการ Type Inference จาก Schema
- ต้องการใช้ Schema เดียวกันทั้ง Client และ Server
- ต้อง Validate API Payload
- ต้อง Validate Environment Variables หรือ Configuration
- ต้องการลดการประกาศ Type ซ้ำ
ตัวอย่าง
const schema = z.object({
email: z.string().email(),
});
type Data = z.infer<typeof schema>;
Schema จึงสามารถทำหน้าที่เป็นทั้ง
Runtime Validation
+
TypeScript Type
ได้ในจุดเดียว
#16. ตัวอย่างโครงสร้าง Project
สามารถแยก Schema ออกจาก Component
src/
├── components/
│ └── RegisterForm.tsx
│
├── schemas/
│ └── register.schema.ts
│
├── services/
│ └── auth.service.ts
│
└── types/
ตัวอย่าง
// schemas/register.schema.ts
import { z } from "zod";
export const registerSchema = z.object({
name: z.string().min(3),
email: z.string().email(),
password: z.string().min(8),
});
export type RegisterInput =
z.infer<typeof registerSchema>;
จากนั้น Component สามารถ Import มาใช้ได้
import {
registerSchema,
RegisterInput,
} from "../schemas/register.schema";
ข้อดีคือ Business Rule ของ Validation ไม่กระจัดกระจายอยู่ใน JSX
#17. Validation ไม่ควรมีเฉพาะ Frontend
แม้จะใช้ Formik, Yup หรือ Zod แล้ว ฝั่ง Backend ยังต้อง Validate ข้อมูลอีกครั้ง
เพราะ Client-side Validation สามารถถูกข้ามได้
Browser
|
Formik + Yup/Zod
|
v
HTTP Request
|
v
Backend API
|
Backend Validation
|
v
Database
หลักการสำคัญคือ
Frontend Validation ช่วย User Experience แต่ Backend Validation เป็นส่วนหนึ่งของ Data Integrity และ Security
#18. สรุป
Formik, Yup และ Zod มีหน้าที่ต่างกัน
Formik
|
+-- จัดการ Form State
+-- touched
+-- errors
+-- submit
+-- loading state
Yup / Zod
|
+-- Validation Schema
+-- Validation Rules
+-- Error Messages
ถ้าใช้ Formik และต้องการเริ่มต้นอย่างรวดเร็ว
Formik + Yup
เป็นคู่ที่ใช้งานง่าย เพราะ Formik รองรับ Yup ผ่าน validationSchema โดยตรง
แต่หาก Project เป็น TypeScript-first และต้องการ Schema ที่นำกลับไปใช้ได้ในหลาย Layer เช่น Frontend, API และ Backend
Formik + Zod
จะมีข้อได้เปรียบด้าน Type Safety และ Schema Reuse
แนวทางที่แนะนำสำหรับระบบ TypeScript สมัยใหม่คือแยก Validation Schema ออกจาก UI Component เพื่อให้กฎข้อมูลสามารถทดสอบและนำกลับไปใช้ซ้ำได้ง่าย
#References
- Formik Validation Guide: https://formik.org/docs/guides/validation
- Yup: https://github.com/jquense/yup
- Zod: https://zod.dev/