0
0
Lập trình
Harry Tran
Harry Tran106580903228332612117

Mẫu Form Tạo và Chỉnh Sửa Tái Sử Dụng với React Hook Form và Zod

Đăng vào 1 tháng trước

• 11 phút đọc

Mẫu Form Tạo và Chỉnh Sửa Tái Sử Dụng với React Hook Form và Zod

Forms trong React từ lâu đã là nỗi ám ảnh đối với nhiều lập trình viên, đặc biệt là khi cần thiết lập validation và giá trị mặc định cho các form. Trong bài viết này, chúng ta sẽ tìm hiểu cách tối ưu hóa việc quản lý form trong React bằng cách kết hợp React Hook Form và Zod để tạo ra các form có thể tái sử dụng cho cả việc tạo và chỉnh sửa mà không cần phải viết lại mã.

Giới thiệu

Khi bắt đầu một dự án mới, việc thiết lập form luôn là một thách thức lớn. Mỗi lần cần cập nhật form, tôi lại phải kiểm tra qua nhiều tệp để đảm bảo mọi thứ vẫn hoạt động. Điều này trở nên tồi tệ hơn khi tôi bắt đầu xây dựng các form phức tạp cho các hệ thống điểm số đa nền tảng như Instagram và YouTube. Giải pháp đơn giản là gộp lại schema và giá trị mặc định trong một hook tùy chỉnh để xử lý cả form tạo và chỉnh sửa mà không gây ra sự dư thừa.

Tóm lại: Hãy kết hợp schema Zod và giá trị mặc định trong một hook tùy chỉnh để quản lý cả form tạo và chỉnh sửa một cách hiệu quả.

Vấn Đề: Đau Đầu Vì Sự Trùng Lặp Form

Nếu bạn đã làm việc với các form trong React, bạn có thể đã gặp phải tình huống này:

  1. Bạn tạo một form với validation.
  2. Sau đó, bạn cần một phiên bản "chỉnh sửa" của cùng một form.
  3. Bạn sao chép hầu hết mã của mình nhưng cần xử lý các giá trị mặc định khác nhau.
  4. Một yêu cầu thay đổi và bạn phải cập nhật cả hai form.
  5. Bạn quên cập nhật một trong hai form và lỗi xảy ra.
  6. Lặp lại cho đến khi bạn tự hỏi về sự nghiệp của mình.

Tôi đã mệt mỏi với việc duy trì các cấu hình form gần như giống hệt nhau giữa các thành phần khác nhau. Nhất là khi sử dụng các thư viện như React Hook Form và Zod, cảm giác như tôi đang lặp lại nỗ lực mà không có lý do chính đáng.

Giải Pháp: Hook Tùy Chỉnh Schema

Dưới đây là phiên bản đơn giản hóa của giải pháp mà tôi đã phát triển:

javascript Copy
import { useMemo } from "react";
import { z } from "zod";

export const useProfileSchema = ({ data = {} } = {}) => {
  return useMemo(() => {
    // Định nghĩa schema validation một lần
    const schema = z.object({
      username: z.string().min(1, { message: "Tên người dùng là bắt buộc" }),
      age: z.number().min(18, { message: "Phải ít nhất 18 tuổi" }),
      subscribe: z.boolean().default(false),
    });

    // Tạo giá trị mặc định dựa trên dữ liệu có sẵn hoặc giá trị trống
    const defaults = {
      username: data?.username || "",
      age: data?.age || 18,
      subscribe: data?.subscribe || false,
    };

    return { schema, defaults };
  }, [data]);
};

Điều thú vị nằm ở tham số data. Khi tạo một form mới, bạn không truyền gì cả. Khi chỉnh sửa một bản ghi hiện tại, bạn truyền vào các giá trị hiện tại. Hook sẽ xử lý mọi thứ!

Thực Thi Giải Pháp

Khi tôi đã nắm vững mô hình này, việc thiết lập form trở nên dễ dàng hơn rất nhiều. Tôi chỉ cần gọi hook này, kết nối schema của nó với React Hook Form thông qua resolver, và sử dụng các giá trị mặc định để khởi tạo form:

javascript Copy
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useProfileSchema } from "./useProfileSchema";

const ProfileForm = ({ initialData = {}, onSubmit }) => {
  const { schema, defaults } = useProfileSchema({ data: initialData });

  const form = useForm({
    resolver: zodResolver(schema),
    defaultValues: defaults,
  });

  return (
    <form onSubmit={form.handleSubmit(onSubmit)}>
      <div className="form-group">
        <label htmlFor="username">Tên người dùng</label>
        <input 
          id="username"
          {...form.register("username")} 
          placeholder="Nhập tên người dùng" 
          className="form-control"
        />
        {form.formState.errors.username && (
          <span className="error">{form.formState.errors.username.message}</span>
        )}
      </div>

      <div className="form-group">
        <label htmlFor="age">Tuổi</label>
        <input 
          id="age"
          type="number" 
          {...form.register("age", { valueAsNumber: true })} 
          placeholder="Nhập tuổi" 
          className="form-control"
        />
        {form.formState.errors.age && (
          <span className="error">{form.formState.errors.age.message}</span>
        )}
      </div>

      <div className="form-check">
        <input 
          id="subscribe"
          type="checkbox" 
          {...form.register("subscribe")} 
          className="form-check-input"
        />
        <label htmlFor="subscribe" className="form-check-label">
          Đăng ký nhận bản tin
        </label>
      </div>

      <button type="submit" className="btn btn-primary mt-3">
        {initialData.id ? "Cập nhật hồ sơ" : "Tạo hồ sơ"}
      </button>
    </form>
  );
};

Điều đẹp đẽ ở đây là: Thành phần này có thể hoạt động cho cả việc tạo và chỉnh sửa hồ sơ. Bạn chỉ cần thay đổi việc truyền vào initialData hoặc không:

javascript Copy
// Tạo hồ sơ mới
<ProfileForm onSubmit={handleCreate} />

// Chỉnh sửa hồ sơ hiện tại
<ProfileForm initialData={existingProfile} onSubmit={handleUpdate} />

Lợi Ích Thực Tế

Sau khi áp dụng mô hình này trên nhiều dự án, tôi đã thu được các lợi ích sau:

  1. Nguồn duy nhất của sự thật: Các quy tắc validation và giá trị mặc định sống cùng nhau, do đó không bao giờ bị mất đồng bộ.
  2. Mã DRY: Không còn việc sao chép và dán logic form giữa các trang tạo và chỉnh sửa.
  3. Duy trì dễ dàng hơn: Cần thay đổi một quy tắc validation? Thực hiện một lần trong hook, không cần ở nhiều nơi.
  4. Tổ chức mã tốt hơn: Tất cả logic liên quan đến form đều có một vị trí rõ ràng.
  5. Chuyển giao dễ dàng hơn: Các thành viên mới trong nhóm có thể nhanh chóng hiểu cách thức hoạt động của các form trong mã nguồn của chúng tôi.

Đi Xa Hơn: Các Mô Hình Nâng Cao

Đối với các dự án lớn hơn, tôi đã mở rộng mô hình này theo nhiều cách đã chứng minh là vô giá:

1. Thêm TypeScript để Tăng Cường An Toàn

Thêm các định nghĩa TypeScript làm cho mô hình này mạnh mẽ hơn:

javascript Copy
// Định nghĩa kiểu dữ liệu cho schema
 type ProfileFormData = {
   username: string;
   age: number;
   subscribe: boolean;
};

 export const useProfileSchema = ({ data = {} as Partial<ProfileFormData> } = {}) => {
   return useMemo(() => {
     const schema = z.object({
       username: z.string().min(1, { message: "Tên người dùng là bắt buộc" }),
       age: z.number().min(18, { message: "Phải ít nhất 18 tuổi" }),
       subscribe: z.boolean().default(false),
     });

     const defaults: ProfileFormData = {
       username: data?.username || "",
       age: data?.age || 18,
       subscribe: data?.subscribe || false,
     };

     return { schema, defaults };
   }, [data]);
};

2. Thêm Hàm Chuyển Đổi

Đôi khi dữ liệu từ API không hoàn toàn phù hợp với những gì form của bạn cần. Việc thêm các hàm chuyển đổi vào hook của bạn có thể giải quyết vấn đề này:

javascript Copy
export const useProfileSchema = ({ data = {} } = {}) => {
   return useMemo(() => {
     // Định nghĩa schema...

     // Chuyển đổi dữ liệu API sang định dạng form
     const apiToForm = (apiData) => ({
       username: apiData.user_name,
       age: apiData.user_age,
       subscribe: Boolean(apiData.newsletter_opt_in),
     });

     // Chuyển đổi dữ liệu form trở lại định dạng API
     const formToApi = (formData) => ({
       user_name: formData.username,
       user_age: formData.age,
       newsletter_opt_in: formData.subscribe ? 1 : 0,
     });

     const defaults = data ? apiToForm(data) : {
       username: "",
       age: 18,
       subscribe: false,
     };

     return { schema, defaults, apiToForm, formToApi };
   }, [data]);
};

3. Hỗ Trợ Validation Điều Kiện

Đối với các form phức tạp hơn mà quy tắc validation thay đổi dựa trên các trường khác:

javascript Copy
export const usePaymentSchema = ({ data = {} } = {}) => {
   return useMemo(() => {
     const schema = z.object({
       paymentMethod: z.enum(["credit", "bank"]),
       creditCardNumber: z.string().optional(),
       bankAccountNumber: z.string().optional(),
     }).refine((data) => {
       if (data.paymentMethod === "credit" && !data.creditCardNumber) {
         return false;
       }
       if (data.paymentMethod === "bank" && !data.bankAccountNumber) {
         return false;
       }
       return true;
     }, {
       message: "Vui lòng điền các thông tin thanh toán cần thiết",
       path: ["paymentMethod"],
     });

     // Phần còn lại của hook...
   }, [data]);
};

4. Đặt Lại Form và Giá Trị Khởi Tạo

Một thách thức mà tôi gặp phải là xử lý việc đặt lại form một cách chính xác. Dưới đây là một mô hình hoạt động tốt:

javascript Copy
const ProfileForm = ({ initialData = {}, onSubmit }) => {
   const { schema, defaults } = useProfileSchema({ data: initialData });

   const form = useForm({
     resolver: zodResolver(schema),
     defaultValues: defaults,
   });

   // Đặt lại form khi initialData thay đổi (hữu ích trong các tình huống chỉnh sửa)
   useEffect(() => {
     form.reset(defaults);
   }, [initialData, form, defaults]);

   const handleReset = () => {
     form.reset(defaults);
   };

   return (
     <form onSubmit={form.handleSubmit(onSubmit)}>
       {/* Các trường form... */}

       <div className="button-group">
         <button type="submit" className="btn btn-primary">
           {initialData.id ? "Cập nhật" : "Tạo"}
         </button>
         <button type="button" onClick={handleReset} className="btn btn-secondary">
           Đặt lại
         </button>
       </div>
     </form>
   );
};

Những Cân Nhắc Về Hiệu Suất

Đối với các form lớn hơn, bạn có thể muốn tối ưu hóa hook của mình hơn nữa:

javascript Copy
export const useProfileSchema = ({ data = {} } = {}) => {
   // Ghi nhớ dữ liệu để tránh tính toán không cần thiết
   const memoizedData = useMemo(() => data, [
     // Chỉ bao gồm các khóa mà bạn quan tâm
     data.username,
     data.age,
     data.subscribe
   ]);

   return useMemo(() => {
     // Logic schema và giá trị mặc định
     return { schema, defaults };
   }, [memoizedData]); // Sử dụng dữ liệu đã ghi nhớ
};

Kết Luận

Thực sự, việc thay đổi nhỏ này đã làm cho quy trình làm việc của tôi trở nên mượt mà hơn rất nhiều. Nếu bạn đã từng cảm thấy bực bội khi phải quản lý trạng thái và validation của form qua nhiều tệp khác nhau, hãy thử áp dụng mô hình này. Nó đã giúp tôi tiết kiệm rất nhiều thời gian và căng thẳng, và tôi tin rằng nó cũng sẽ giúp bạn.

Điều tuyệt vời nhất? Mô hình này có thể hoạt động với bất kỳ thư viện form nào trong React - không chỉ riêng React Hook Form. Nguyên tắc kết hợp schema và giá trị mặc định trong một hook tùy chỉnh là có thể áp dụng rộng rãi.

Bạn đã tìm thấy những mô hình form nào hữu ích trong các dự án React của mình? Hãy cho tôi biết trong phần bình luận!


Theo dõi tôi để nhận thêm nhiều mẹo và mẫu React giúp việc phát triển trở nên dễ dàng và thú vị hơn. Nếu bài viết này hữu ích với bạn, hãy xem xét chia sẻ với nhóm của bạn!

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào