#การทำ 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 ได้หลายวิธี เช่น

  1. เขียน validate() เอง
  2. Field-level validation
  3. ใช้ Schema Validation
  4. ใช้ 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