+1

Tạo logic tái sử dụng với Custom Hooks trong React

Các Hook tích hợp sẵn của React như useState và useEffect cho phép quản lý state và các tính năng lifecycle trực tiếp trong các functional component. Tuy nhiên, khi cần logic tái sử dụng trên nhiều component, custom Hooks cung cấp một giải pháp để giữ cho code gọn gàng và dễ mở rộng.

Khái niệm và lợi ích của React Hooks

React Hooks là gì? Các Hook tích hợp sẵn của React như useState và useEffect cho phép quản lý state và các tính năng lifecycle trực tiếp trong functional component. Nhưng khi chúng ta cần logic tái sử dụng trên nhiều component, custom Hooks cung cấp một cách để giữ cho code được tổ chức và dễ mở rộng.

Hook hữu ích như thế nào và khi nào nên sử dụng chúng? Hook đóng gói logic tái sử dụng, giúp dễ dàng quản lý state và các tính năng lifecycle trên nhiều component. Custom Hooks đặc biệt hữu ích để xử lý forms, lấy dữ liệu và chia sẻ state, giảm sự dư thừa và làm cho codebase có thể mở rộng và dễ bảo trì.

Điều gì tạo nên một hàm là 'Hook' trong React? Một hàm được coi là Hook trong React nếu nó đóng gói logic stateful có thể tái sử dụng và tuân theo quy ước đặt tên bằng cách thêm tiền tố "use". Tiền tố "use" báo hiệu cho React rằng đó là một Hook, cho phép nó áp dụng các quy tắc nội bộ đặc biệt (sẽ được đề cập sau) để quản lý hành vi của hàm.

Ví dụ về ứng dụng Custom Hooks trong quản lý Form

Ví dụ ứng dụng của chúng ta bao gồm các forms thu thập thông tin người dùng. Nó có hai phần: 'Thông tin cá nhân' và 'Thông tin thanh toán'. Mỗi phần thu thập dữ liệu và yêu cầu xác thực. Dưới đây là cách custom hooks đơn giản hóa việc xử lý và xác thực form.

Đây là cấu trúc của ứng dụng, chúng ta sẽ xem xét cụ thể các tệp này: PersonalDetailsForm.jsx để định nghĩa các trường form, useForm.js là custom Hook để quản lý state và gửi form, và useValidation.js là custom Hook cho logic xác thực tập trung. image.png

1. PersonalDetailsForm.jsx – Sử dụng useForm và lược đồ xác thực

Đây là tệp thành phần cho biểu mẫu 'Thông tin cá nhân'.

import { Input, Dropdown, Button } from "@/components";
import useForm from "@/hooks/useForm";
import { validations } from "@/util/validationHelper";

const PersonalDetailsForm = ({ title }) => {
  const initialData = {
    FirstName: "",
    LastName: "",
    Email: "",
    ReceiveMarketing: false
  };

  // Validations
  const validationSchema = {
    FirstName: [
      {
        validator: validations.required,
        message: "First name is required"
      },
      {
        validator: (value) => validations.minLength(value, 2),
        message: "First name must be at least two characters"
      }
    ],
    LastName: [
      {
        validator: validations.required,
        message: "Last name is required"
      }
    ],
    Email: [
      {
        validator: validations.required,
        message: "Email is required"
      },
      { validator: validations.email, message: "Invalid email" }
    ]
  };

  // Custom Hook useForm for state management and validation handling
  const { formData, handleInputChange, handleSubmit, validationProps } =
    useForm(initialData, () => console.log(formData), validationSchema);

  return (
    <>
      <div className="bg-white rounded-md p-6">
        <form onSubmit={handleSubmit}>
          <h2 className="text-2xl font-semibold text-orange-600 pb-4">
            {title}
          </h2>
          <Input
            label="First Name"
            placeholder="John"
            name="FirstName"
            onChange={handleInputChange}
            value={formData.FirstName}
            {...validationProps("FirstName")}
          />
          <Input
            label="Last Name"
            placeholder="Doe"
            name="LastName"
            onChange={handleInputChange}
            value={formData.LastName}
            {...validationProps("LastName")}
          />
          <Input
            label="Email"
            placeholder="Email"
            name="Email"
            onChange={handleInputChange}
            value={formData.Email}
            {...validationProps("Email")}
          />

          <Dropdown
            label="Would you like to receive marketing emails from us?"
            name="ReceiveMarketing"
            onChange={handleInputChange}
            value={formData.ReceiveMarketing}
            items={[
              { label: "Yes", value: true },
              { label: "No", value: false }
            ]}
            isRequired={false}
            isValid={true}
          />

          <Button type="submit" text="Submit" />
        </form>
      </div>
    </>
  );
};

export default PersonalDetailsForm;

Trong PersonalDetailsForm.jsx, chúng ta khởi tạo initialData để định nghĩa giá trị trường và sử dụng validationSchema để chỉ định các quy tắc xác thực. Chúng được truyền cho custom Hook useForm, Hook này cung cấp các hàm như handleInputChange để cập nhật state form và validationProps để xử lý lỗi và hiển thị thông báo lỗi cho người dùng.

2. useForm.js Custom Hook – Xử lý trạng thái biểu mẫu và gửi

Custom Hook useForm quản lý state form với formData và xử lý việc gửi form. Các thay đổi trường form được xử lý bằng handleInputChange. Hàm này phân tích cú pháp các kiểu dữ liệu khác nhau (như text, checkbox và number) và xác thực từng trường khi thay đổi, giữ cho component PersonaDetailsForm của chúng ta gọn gàng.

import { useState } from "react";
import useValidation from "@/hooks/useValidation";

const useForm = (initialValues, onSubmitCallback, validationSchema = {}) => {
  // Set form data state
  const [formData, setFormData] = useState(initialValues);

  // Calling another custom Hook - useValidation
  const { validateFieldOnChange, validateAllFields, validationProps } =
    useValidation(formData, validationSchema);

  const handleInputChange = (e) => {
    const { name, type, value, checked } = e.target;

    const parsedValue =
      type === "checkbox"
        ? checked
        : value === "true" // Convert string representation of boolean to boolean
        ? true
        : value === "false"
        ? false
        : type === "number"
        ? value === ""
          ? ""
          : parseFloat(value) // Parse as float if it's a number, allow empty string (for when user clears input)
        : value; // e.target.value

    setFormData({
      ...formData,
      [name]: parsedValue
    });

    validateFieldOnChange(name, value);
  };

  const resetForm = () => {
    const resetValues = Object.keys(formData).reduce((acc, key) => {
      acc[key] = "";
      return acc;
    }, {});

    setFormData(resetValues);
  };

  const handleSubmit = async (e) => {
    e.preventDefault();

    if (validateAllFields()) {
      try {
        await onSubmitCallback(formData);
      } catch (error) {
        console.error("Error during form submission", error);
      }
    } else {
      console.warn("Validation Failed");
      alert("Invalid Form");
    }
  };

  return {
    formData,
    handleInputChange,
    resetForm,
    handleSubmit,
    validationProps
  };
};

export default useForm;

3. useValidation.js Custom Hook - Logic xác thực tập trung

Custom Hook useValidation tập trung logic xác thực, với các hàm như validateFieldOnChange để kiểm tra các trường trong thời gian thực và validateAllFields để xác nhận tất cả các trường đáp ứng các yêu cầu trước khi gửi.

Sử dụng validationProps, chúng ta có thể dễ dàng đính kèm phản hồi xác thực vào từng trường. Hãy xem các component input trong PersonalDetailsForm.jsx để xem cách các validation props được trải rộng.

import { useState, useMemo } from "react";

const useValidation = (formData, validationSchema) => {
  const [errors, setErrors] = useState({});

  const memoValidationSchema = useMemo(
    () => validationSchema,
    [validationSchema]
  );

  // Validate field on change
  const validateFieldOnChange = (name, value) => {
    if (memoValidationSchema[name]) {
      const fieldErrors = validateField(value, memoValidationSchema[name]);
      setErrors((prevErrors) => ({
        ...prevErrors,
        [name]: fieldErrors
      }));
    }
  };

  // Validate field against validation schema
  const validateField = (value, rules) => {
    let errors = [];

    for (const rule of rules) {
      if (
        typeof rule === "object" &&
        rule.validator &&
        !rule.validator(value)
      ) {
        errors.push(rule.message); // Add error message if validation fails
      }
    }

    return errors;
  };

  // Validate all fields against their validation schema
  const validateAllFields = () => {
    const newErrors = [];
    for (const field in memoValidationSchema) {
      let fieldErrors = validateField(
        formData[field],
        memoValidationSchema[field]
      );
      if (fieldErrors.length > 0) {
        newErrors[field] = fieldErrors;
      }
    }
    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };

  // Validation props to use in form field component for error handling
  const validationProps = useMemo(
    () => (name) => ({
      isValid: !errors?.[name] || errors?.[name].length === 0,
      errorMessage: errors?.[name]?.[0]
    }),
    [errors]
  );

  return {
    validateFieldOnChange,
    validateAllFields,
    validationProps
  };
};

export default useValidation;

4. PaymentDetailsForm.jsx – Khả năng tái sử dụng

Đây là tệp component cho form 'Thông tin thanh toán'. Thiết lập của chúng ta cho phép chúng ta tái sử dụng useForm và useValidation trong bất kỳ component form nào như PaymentDetailsForm.jsx mà không cần sao chép logic.

Còn nếu không thì sao? Nếu không có custom Hooks, mỗi form sẽ cần logic quản lý state và xác thực riêng, dẫn đến code lặp lại và lộn xộn. Custom Hooks tập trung chức năng này, cung cấp một phương pháp tái sử dụng tuân theo nguyên tắc DRY (Don't Repeat Yourself - Không lặp lại chính mình).

Quy tắc sử dụng Hooks và kết luận

Để đảm bảo rằng Hooks hoạt động đáng tin cậy, React thực thi một vài quy tắc chính để duy trì hành vi nhất quán trên các component.

  • Luôn gọi Hooks ở cấp cao nhất của hàm React của bạn: Không sử dụng Hooks bên trong vòng lặp, điều kiện, hàm lồng nhau hoặc sau khi trả về có điều kiện. Điều này đảm bảo rằng Hooks được gọi theo cùng một thứ tự trên mỗi lần render, điều mà React dựa vào để theo dõi state một cách chính xác.
  • Không gọi Hooks trong trình xử lý sự kiện hoặc class component: Hooks được thiết kế đặc biệt để sử dụng trong functional component hoặc trong custom Hooks, việc sử dụng chúng ở những nơi khác có thể dẫn đến hành vi không mong muốn.
  • Chỉ sử dụng Hooks trong các hàm React hoặc custom Hooks: Custom Hooks có thể gọi các Hooks khác, tuân theo các quy tắc tương tự, như trong ví dụ của chúng ta, nơi Hook useForm gọi Hook useValidation.

Tính tái sử dụng và khả năng bảo trì là cần thiết để xây dựng các ứng dụng có thể mở rộng. Custom Hooks trong React cho phép chúng ta đạt được điều này bằng cách đóng gói logic có thể được chia sẻ trên các component. Điều này dẫn đến code sạch hơn, theo module hơn, dễ hiểu, kiểm tra và mở rộng hơn.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí