توسعه Custom Hook برای مدیریت فرم‌ها در React

کتابخانه‌های بسیاری وجود دارند که می‌توان برای ایجاد و مدیریت State فرم‌ها در React از آن‌ها استفاده کرد. در این مطلب، با نحوه‌ی ساختن یک Custom Hook برای مدیریت فرم‌ها در React بدون نیاز به هیچ کتابخانه‌ای آشنا خواهیم شد.

در این مقاله هوکی خواهیم‌ ساخت که نه‌ تنها المنت‌های input متعلق به یک فرم را رندر می‌کند بلکه اعتبار سنجی آن‌ها را نیز کنترل می‌کند. این روش برای پیاده سازی فرم‌های مختلف در دوره جامع MERN Stack استفاده شده است.

یک فرم ثبت‌ نام شامل فیلدهای ورودی زیر خواهیم داشت:

  • نام
  • ایمیل
  • رمز عبور
  • تکرار رمز عبور

در مرحله اول، به کامپوننتی برای نشان دادن المنت‌های input در فرم خود نیاز داریم.

 

function InputField(props) {
  const {
    label,
    type,
    name,
    handleChange,
    errorMessage,
    isValid,
    value,
  } = props;

  return (
    <div className="inputContainer">
      <label>{label}</label>
      <input type={type} name={name} value={value} onChange={handleChange} />
      {errorMessage && !isValid && (
        <span className="error">{errorMessage}</span>
      )}
    </div>
  );
}

 

کامپوننت InputField برای پیکربندی (configure) هر المنت input به props های مختلفی نیاز دارد که در فرم ما رندر خواهند‌ شد.

هر ورودی یک label و یک پیام خطای (Error Message) مرتبط با خود را دارد. پیام خطا زمانی نمایش داده می‌شود که errorMessage prop شامل یک پیام برای نمایش بوده و فیلد ورودی معتبر نباشد.

برای کامپوننت InputField، استایل‌های زیر را در نظر گرفته‌ایم.

 

.inputContainer {
  display: flex;
  flex-direction: column;
  margin: 0 0 15px;
}

label {
  margin: 0 0 6px 0;
  font-size: 1.1rem;
}

input {
  padding: 10px;
  border: none;
  border-bottom: 1px solid #777;
  background-color: #eee;
  outline: none;
  font-size: 1.1rem;
  box-sizing: border-box;
  margin: 0 0 8px 0;
}

.error {
  color: red;
}

 

همانطور که اشاره کردیم، هوک قرار است المنت‌های ورودی داخل فرم را رندر کند. برای همین باید ساختار آبجکتی فرم را ایجاد کنیم.

ساختار آبجکتی فرم را به شکل زیر مشخص می‌کنیم.

 

{
  renderInput: (handleChange, value, isValid, error, key) => {
    // return the JSX code that will
    // render the input component, passing
    // in the required props to Input component
  },
  label: 'input label',
  value: 'default value for the input',
  valid: false,
  errorMessage: "",
  touched: false,
  validationRules: [
    /* array of objects representing validation rules */
  ]
}

 

از آنجائیکه بیش از یک فیلد ورودی با استفاده از ساختار فوق نمایش داده خواهد‌ شد، یک تابع کمکی می‌سازیم که چندین پارامتر را به عنوان ورودی گرفته و یک آبجکت را که نمایش‌ دهنده‌ی یک فیلد ورودی تکی در فرم ما است، به عنوان خروجی بر می‌گرداند.

 

import React from 'react';
import Input from '../components/Input';

function createFormFieldConfig(label, name, type, defaultValue = '') {
  return {
    renderInput: (handleChange, value, isValid, error, key) => {
      return (
        <Input
          key={key}
          name={name}
          type={type}
          label={label}
          isValid={isValid}
          value={value}
          handleChange={handleChange}
          errorMessage={error}
        />
      );
    },
    label,
    value: defaultValue,
    valid: false,
    errorMessage: '',
    touched: false,
  };
}

 

تابع renderInput توسط هوک سفارشی ما برای رندر کردن کامپوننت‌های InputField فرم و انتقال propsهای موردنیاز توسط کامپوننت InputField استفاده خواهد‌ شد. این تابع پارامترهای زیر را می‌گیرد:

handleChange: تابعی برای فراخوانی شدن در زمان onChange فیلد‌های ورودی.

value: مقدار فیلد ورودی.

isValid: یک مقدار true یا false مشخص کننده‌ی معتبر بودن یا نبودن مقدار ورودی.

error: پیغام خطا برای نمایش دادن هنگام نامعتبر بودن مقدار ورودی.

key: کامپوننت‌های Input ما توسط هوک و با استفاده از یک حلقه رندر می‌شوند، برای همین لازم است یک key به هر یک از کامپوننت‌های Input اختصاص دهیم.

آبجکتی که توسط createFormFieldConfig برگردانده می‌شود، شامل ویژگی validationRules که در ساختار قبلی نوشته شده بود نیست. این ویژگی را به آبجکت‌هایی که نشانگر فیلد‌های ورودی در فرم هستند، با داشتن قوانین اعتبار سنجی اضافه خواهیم کرد.

در این قسمت نمایش آبجکتی فرم را می‌نویسیم. این آبجکت را در همان فایلی که تابع کمکی createFormFieldConfig نوشته شده بود، ایجاد می‌کنیم.

 

export const signupForm = {
  name: {
    ...createFormFieldConfig('Full Name', 'name', 'text'),
  },
  email: {
    ...createFormFieldConfig('Email', 'email', 'email'),
  },
  password: {
    ...createFormFieldConfig('Password', 'password', 'password'),
  },
  confirmPassword: {
    ...createFormFieldConfig('Confirm Password', 'confirmPassword', 'password'),
  },
};

 

اکنون Custom Hook خود را توسعه می‌دهیم. هدف ما در این قسمت نوشتن کدی است که ما را قادر سازد تا هوک مورد نظر را در فرم خود استفاده کرده و کامپوننت‌های InputField را با این هوک رندر کنیم.

 

import { useState, useCallback } from 'react';

function useForm(formObj) {
  const [form, setForm] = useState(formObj);

  function renderFormInputs() {
    return Object.values(form).map((inputObj) => {
      const { value, label, errorMessage, valid, renderInput } = inputObj;
      return renderInput(onInputChange, value, valid, errorMessage, label);
    });
  }

  const onInputChange = useCallback((event) => {}, []);

  return { renderFormInputs };
}

export default useForm;

 

در این قسمت، کامپوننتی برای نمایش فرم signup خود ایجاد می‌کنیم.

 

import React from 'react';
import useForm from './useForm';
import { signupForm } from './utils/formConfig';

import './SignupForm.css';

export default function SignupForm() {
  const { renderFormInputs } = useForm(signupForm);

  return (
    <form className="signupForm">
      <h1>Sign Up</h1>

      {renderFormInputs()}

      <button type="submit">Submit</button>
    </form>
  );
}

 

در این کد، فرم آبجکتی signup را که در فایل جداگانه‌ای نوشته‌ایم و هوک را import کرده‌ایم.

در داخل کامپوننت، هوک useForm را به آبجکتی که فرم ما را نمایش می‌دهد افزوده‌ایم. از آبجکتی که توسط این هوک برگردانده می‌شود، تابعی به نام renderFormInputs را بدست می‌آوریم که در داخل فرم خود برای رندر کردن input ها فراخوانی خواهد شد.

استایل های این فرم را در ادامه مشاهده می‌کنید.

 

.signupForm {
  max-width: 400px;
  box-shadow: 0 0 4px rgba(0, 0, 0, 0.3);
  margin: 20px auto;
  padding: 20px;
}

.signupForm h1 {
  margin: 0 0 20px;
  text-align: center;
}

button {
  padding: 10px 15px;
  border-radius: 4px;
  border: none;
  box-shadow: 0 0 4px rgba(0, 0, 0, 0.4);
  width: 150px;
  background: blueviolet;
  color: #fff;
  cursor: pointer;
}

button:disabled {
  background: #eee;
  color: #999;
  box-shadow: none;
}

 

در این مرحله فرمی داریم که از Custom Hook ما برای نمایش کامپوننت‌های Input استفاده می‌کند.

هنوز نمی‌توانیم مقادیر فیلد‌های input را تغییر دهیم. چرا که هنوز کنترل رویداد مربوط به onChange را در هوک خود پیاده سازی نکرده‌ایم. این تابع را زمانی پیاده‌سازی خواهیم کرد که قوانین اعتبار سنجی را برای ورودی‌‌های فرم نوشته‌ باشیم.

برای این که بتوانیم از این قوانین برای اعتبار سنجی ورودی‌ها و نشان دادن پیام‌های خطا زمانی که کاربر مقدار نامعتبر در ورودی بنویسد، استفاده کنیم.

هر قانون اعتبار سنجی یک آبجکت است که نشان‌دهنده‌ی قانونی است که توسط هوک ما برای اعتبارسنجی هر فیلد ورودی در فرم استفاده می‌شود. ساختار آبجکت به این صورت خواهد بود.

 

{
  name: 'name of the rule',
  message: 'error message to show when input validation fails',
  validate: <validation function>
}

 

قوانین زیر را برای فرم در نظر گرفته‌ایم:

required: مقدار ورودی هر فیلد الزامی است.

minimum input length: مقدار ورودی هر فیلد باید شامل حداقل تعداد کاراکتر مشخصی باشد.

maximum input length: مقدار ورودی هر فیلد باید کمتر از حداکثر تعداد کاراکتر مشخص باشد.

password match rule: مقادیر فیلد رمز‌عبور و فیلد تکرار رمز‌عبور باید باهم برابر باشند.

در ادامه تابع کمکی را که به ما در ساختن هر یک از قوانین اعتبارسنجی کمک خواهد کرد، ایجاد می‌کنیم.

 

function createValidationRule(ruleName, errorMessage, validateFunc) {
  return {
    name: ruleName,
    message: errorMessage,
    validate: validateFunc,
  };
}

 

اکنون قوانین اعتبار سنجی را در همان فایل که شامل تابع createValidationRule است، ایجاد می‌کنیم.

 

export function requiredRule(inputName) {
  return createValidationRule(
    'required',
    `${inputName} required`,
    (inputValue, formObj) => inputValue.length !== 0
  );
}

export function minLengthRule(inputName, minCharacters) {
  return createValidationRule(
    'minLength',
    `${inputName} should contain atleast ${minCharacters} characters`,
    (inputValue, formObj) => inputValue.length >= minCharacters
  );
}

export function maxLengthRule(inputName, maxCharacters) {
  return createValidationRule(
    'minLength',
    `${inputName} cannot contain more than ${maxCharacters} characters`,
    (inputValue, formObj) => inputValue.length <= maxCharacters
  );
}

export function passwordMatchRule() {
  return createValidationRule(
    'passwordMatch',
    `passwords do not match`,
    (inputValue, formObj) => inputValue === formObj.password.value
  );
}

 

هر تابعی، تابع createValidationRule را به همراه آرگومان‌های الزامی فراخوانی می‌کند.

هر تابع به جز آخرین تابع یعنی passwordMatchRule، یک پارامتر به نام inputName می‌گیرد که نام ورودی‌ای است که این قانون با آن مرتبط خواهد شد.

توابع minLengthRule و maxLengthRule نیز آرگومان دوم می‌گیرند که تعداد حداقل و حداکثر کاراکترها را به ترتیب مشخص می‌کند.

تابع اعتبار سنجی هر قانون یک مقدار true یا false باز می‌گرداند.

تابع اعتبار سنجی برای requiredRule، بررسی می‌کند که آیا مقدار ورودی متعلق به فیلد خالی است یا نه.

تابع اعتبار سنجی minLengthRule، بررسی می‌کند که آیا طول مقدار ورودی حداقل برابر یا بیشتر از تعداد کاراکترهای مشخص شده‌ است یا خیر. به‌طورمشابه، تابع maxLengthRule بررسی می‌کند که آیا طول مقدار فیلد ورودی از تعداد مشخص‌ شده‌ی کاراکترها کمتر یا برابر است.

تابع passwordMatchRule بررسی می‌کند که آیا مقادیر فیلد‌های رمز‌عبور و تکرار رمز‌عبور با هم برابر هستند یا نه.

به توابع اعتبارسنجی دو آرگومان زیر داده می‌شود:

inputValue: مقدار ورودی فیلد که این قانون با آن مرتبط است.

formObj: نمایش فرم به صورت آبجکت که در کد این آبجکت تنها توسط تابع اعتبار سنجی passwordMatchRule استفاده می‌شود.

اکنون که قوانین اعتبارسنجی را نوشتیم، این قوانین را روی آبجکت متعلق به فرم signup اضافه می‌کنیم.

 

import {
  requiredRule,
  minLengthRule,
  maxLengthRule,
  passwordMatchRule,
} from './inputValidationRules';

export const signupForm = {
  name: {
    ...createFormFieldConfig('Full Name', 'name', 'text'),
    validationRules: [
      requiredRule('name'),
      minLengthRule('name', 3),
      maxLengthRule('name', 25),
    ],
  },
  email: {
    ...createFormFieldConfig('Email', 'email', 'email'),
    validationRules: [
      requiredRule('email'),
      minLengthRule('email', 10),
      maxLengthRule('email', 25),
    ],
  },
  password: {
    ...createFormFieldConfig('Password', 'password', 'password'),
    validationRules: [
      requiredRule('password'),
      minLengthRule('password', 8),
      maxLengthRule('password', 20),
    ],
  },
  confirmPassword: {
    ...createFormFieldConfig('Confirm Password', 'confirmPassword', 'password'),
    validationRules: [passwordMatchRule()],
  },
};

 

فیلد confirmPassword تنها passwordMatchRule را لازم دارد زیرا باید با مقدار فیلد password برابر باشد. بنابراین هر قانونی که روی فیلد password اجرا شود باید روی فیلد confirmPassword نیز اجرا شود.

کد تابع onInputChange در هوک به این صورت خواهد بود.

 

const onInputChange = useCallback(
  (event) => {
    const { name, value } = event.target;
    const inputObj = { ...form[name] };
    inputObj.value = value;

    const isValidInput = isInputFieldValid(inputObj);
    if (isValidInput && !inputObj.valid) {
      inputObj.valid = true;
    } else if (!isValidInput && inputObj.valid) {
      inputObj.valid = false;
    }

    inputObj.touched = true;
    setForm({ ...form, [name]: inputObj });
  },
  [form, isInputFieldValid]
);

 

این تابع هر بار که یک ورودی تغییر کند و باعث فعال شدن onChange شود، فراخوانی می‌شود. برای همین در هوک useCallback قرار می‌دهیم تا از ایجاد‌ شدن تابع جدید هر بار که State آپدیت می‌شود و کد داخل این هوک دوباره اجرا می‌شود، جلوگیری کنیم.

این تابع از تابع دیگری به نام isInputFieldValid استفاده می‌کند که یک مقدار true یا false برمی‌گرداند که نشانگر این است که آیا فیلد ورودی‌ای که رویداد onChange را اجرا کرده معتبر است یا نه.

 

const isInputFieldValid = useCallback(
  (inputField) => {
    for (const rule of inputField.validationRules) {
      if (!rule.validate(inputField.value, form)) {
        inputField.errorMessage = rule.message;
        return false;
      }
    }

    return true;
  },
  [form]
);

 

این تابع نیز در داخل هوک قرار گرفته‌ است و یک آبجکت که نشانگر یک المنت input در فرم ما است، می‌گیرد و تابع validate را برای اعتبارسنجی مقادیر فراخوانی می‌کند.

اگر تابع validate مقدار false برگرداند، یک پیام خطا برای آن ورودی تنظیم کرده و مقدار false از این تابع به عنوان خروجی در نظر گرفته می‌شود.

اگر تمام قوانین اعتبار سنجی به درستی انجام شود این تابع مقدار true بر‌می‌گرداند که نشان می‌دهد input معتبر است.

هوک ما تقریبا کامل است. در این قسمت تابعی را پیاده‌سازی می‌کنیم که یک مقدار true یا false برمی‌گرداند که نشان می‌دهد آیا کل فرم معتبر است یا نه.

 

const isFormValid = useCallback(() => {
  let isValid = true;
  const arr = Object.values(form);

  for (let i = 0; i < arr.length; i++) {
    if (!arr[i].valid) {
      isValid = false;
      break;
    }
  }

  return isValid;
}, [form]);

 

این تابع بررسی می‌کند آیا فیلد ورودی نامعتبری در فرم ما وجود دارد یا نه. اگر وجود داشته‌ باشد مقدار false بر‌می‌گرداند. اگر تمام المنت‌های input معتبر باشند مقدار true بر میگرداند که نشان می‌دهد فرم ما معتبر است.

این تابع در کامپوننت SignupForm برای فعال/غیرفعال کردن دکمه submit استفاده می‌شود.

کد هوک useForm به این صورت خواهد بود.

 

import { useState, useCallback } from 'react';

function useForm(formObj) {
  const [form, setForm] = useState(formObj);

  function renderFormInputs() {
    return Object.values(form).map((inputObj) => {
      const { value, label, errorMessage, valid, renderInput } = inputObj;
      return renderInput(onInputChange, value, valid, errorMessage, label);
    });
  }

  const isInputFieldValid = useCallback(
    (inputField) => {
      for (const rule of inputField.validationRules) {
        if (!rule.validate(inputField.value, form)) {
          inputField.errorMessage = rule.message;
          return false;
        }
      }

      return true;
    },
    [form]
  );

  const onInputChange = useCallback(
    (event) => {
      const { name, value } = event.target;
      const inputObj = { ...form[name] };
      inputObj.value = value;

      const isValidInput = isInputFieldValid(inputObj);
      if (isValidInput && !inputObj.valid) {
        inputObj.valid = true;
      } else if (!isValidInput && inputObj.valid) {
        inputObj.valid = false;
      }

      inputObj.touched = true;
      setForm({ ...form, [name]: inputObj });
    },
    [form, isInputFieldValid]
  );

  const isFormValid = useCallback(() => {
    let isValid = true;
    const arr = Object.values(form);

    for (let i = 0; i < arr.length; i++) {
      if (!arr[i].valid) {
        isValid = false;
        break;
      }
    }

    return isValid;
  }, [form]);

  return { renderFormInputs, isFormValid };
}

export default useForm;

 

در ادامه از تابع isFormValid در کامپوننت SignupForm استفاده می کنیم.

 

export default function SignupForm() {
  const { renderFormInputs, isFormValid } = useForm(signupForm);

  return (
    <form className="signupForm">
      <h1>Sign Up</h1>
      {renderFormInputs()}
      <button type="submit" disabled={!isFormValid()}>
        Submit
      </button>
    </form>
  );
}

 

از تابع isFormValid برای مشخص‌کردن این که دکمه submit باید فعال باشد یا نه، استفاده می‌کنیم.

 

[button class=”github-btn” href=”http://frontcast.ir/dark-mode-react-hooks”]ویدیوی آموزشی: توسعه Dark Mode با استفاده از React Hooks[/button]

 

دیدگاه‌ها:

افزودن دیدگاه جدید