Пошук уроків, статей та іншого контенту
Створите типобезпечні форми з полями, подіями введення, валідацією та відправленням даних.
У React форма зазвичай є керованою: значення кожного поля зберігається у стані компонента, а зміна поля оновлює цей стан.
Це дає змогу:
отримувати актуальні дані форми у будь-який момент;
типізувати значення полів;
показувати помилки валідації;
керувати відправленням форми;
блокувати або змінювати інтерфейс залежно від стану форми.
Для TypeScript важливо типізувати:
стан форми;
події onChange;
подію onSubmit;
об’єкт помилок;
результат валідації.
Спочатку опишемо всі поля форми окремим типом:
type FormState = {
name: string;
email: string;
password: string;
bio: string;
};Тепер TypeScript контролює структуру об’єкта:
const initialForm: FormState = {
name: "",
email: "",
password: "",
bio: "",
};Якщо додати поле, якого немає в FormState, або пропустити обов’язкове поле, TypeScript повідомить про помилку.
Стан форми створюємо за допомогою useState:
const [form, setForm] = useState<FormState>(initialForm);Тип FormState гарантує, що form.name, form.email та інші властивості завжди мають тип string.
Для текстових полів React використовує подію ChangeEvent.
Для звичайного <input> тип події такий:
ChangeEvent<HTMLInputElement>Для <textarea>:
ChangeEvent<HTMLTextAreaElement>Приклад обробника для одного поля:
const handleNameChange = (
event: ChangeEvent<HTMLInputElement>,
): void => {
setForm((previousForm) => ({
...previousForm,
name: event.target.value,
}));
};event.target.value має тип string, тому це значення можна записати у поле name.
У React не слід змінювати стан безпосередньо:
// Неправильно
form.name = "Нове ім'я";Замість цього потрібно використовувати функцію оновлення стану:
setForm((previousForm) => ({
...previousForm,
name: "Нове ім'я",
}));Оператор розпилення ...previousForm зберігає значення інших полів.
Для кожного поля можна написати окремий обробник, але це призводить до дублювання коду. Зручніше створити універсальну функцію оновлення.
const updateField = <K extends keyof FormState>(
field: K,
value: FormState[K],
): void => {
setForm((previousForm) => ({
...previousForm,
[field]: value,
}));
};Розглянемо тип параметрів:
K extends keyof FormState означає, що K може бути лише ключем FormState;
field — назва поля;
value: FormState[K] — значення, яке відповідає типу конкретного поля.
Наприклад:
updateField("name", "Олена");
updateField("email", "olena@example.com");А такі виклики TypeScript відхилить:
// Помилка: "unknown" не є ключем FormState
updateField("unknown", "значення");У поточному прикладі всі поля мають тип string, але зв’язок між ключем і значенням все одно залишається типобезпечним. Якщо пізніше поле матиме інший тип, TypeScript допоможе уникнути помилок.
Подія відправлення форми має тип:
FormEvent<HTMLFormElement>Обробник повинен викликати event.preventDefault(), щоб браузер не перезавантажував сторінку:
const handleSubmit = (event: FormEvent<HTMLFormElement>): void => {
event.preventDefault();
console.log(form);
};Без preventDefault() браузер виконає стандартну HTML-відправку форми, і React-компонент може бути перезавантажений.
Для помилок зручно використовувати Partial<Record<...>>:
type FormErrors = Partial<Record<keyof FormState, string>>;Цей тип означає:
ключами можуть бути лише поля FormState;
кожна помилка є необов’язковою;
значення помилки має тип string.
Приклад об’єкта помилок:
const errors: FormErrors = {
email: "Введіть коректну електронну адресу",
};Помилки для інших полів додавати не обов’язково.
Валідацію краще винести в окрему функцію. Вона отримує дані форми та повертає об’єкт помилок:
const validateForm = (values: FormState): FormErrors => {
const errors: FormErrors = {};
if (values.name.trim().length < 2) {
errors.name = "Ім'я має містити щонайменше 2 символи";
}
if (!values.email.includes("@")) {
errors.email = "Введіть коректну електронну адресу";
}
if (values.password.length < 8) {
errors.password = "Пароль має містити щонайменше 8 символів";
}
if (values.bio.trim().length < 20) {
errors.bio = "Опис має містити щонайменше 20 символів";
}
return errors;
};Валідація не змінює стан і не залежить від React, тому її легко перевіряти окремо.
Під час відправлення:
const validationErrors = validateForm(form);
if (Object.keys(validationErrors).length > 0) {
setErrors(validationErrors);
return;
}Якщо об’єкт не містить помилок, форма вважається валідною.
Нижче наведено повний компонент із:
типізованим станом;
типізованими подіями;
універсальним оновленням полів;
валідацією;
відображенням помилок;
типізованим відправленням форми.
import {
useState,
type ChangeEvent,
type FormEvent,
} from "react";
type FormState = {
name: string;
email: string;
password: string;
bio: string;
};
type FormErrors = Partial<Record<keyof FormState, string>>;
type SubmitStatus = "idle" | "success";
const initialForm: FormState = {
name: "",
email: "",
password: "",
bio: "",
};
const validateForm = (values: FormState): FormErrors => {
const errors: FormErrors = {};
if (values.name.trim().length < 2) {
errors.name = "Ім'я має містити щонайменше 2 символи";
}
if (!values.email.includes("@")) {
errors.email = "Введіть коректну електронну адресу";
}
if (values.password.length < 8) {
errors.password = "Пароль має містити щонайменше 8 символів";
}
if (values.bio.trim().length < 20) {
errors.bio = "Опис має містити щонайменше 20 символів";
}
return errors;
};
export default function RegistrationForm() {
const [form, setForm] = useState<FormState>(initialForm);
const [errors, setErrors] = useState<FormErrors>({});
const [submitStatus, setSubmitStatus] =
useState<SubmitStatus>("idle");
const updateField = <K extends keyof FormState>(
field: K,
value: FormState[K],
): void => {
setForm((previousForm) => ({
...previousForm,
[field]: value,
}));
setErrors((previousErrors) => {
const nextErrors = { ...previousErrors };
delete nextErrors[field];
return nextErrors;
});
setSubmitStatus("idle");
};
const handleNameChange = (
event: ChangeEvent<HTMLInputElement>,
): void => {
updateField("name", event.target.value);
};
const handleEmailChange = (
event: ChangeEvent<HTMLInputElement>,
): void => {
updateField("email", event.target.value);
};
const handlePasswordChange = (
event: ChangeEvent<HTMLInputElement>,
): void => {
updateField("password", event.target.value);
};
const handleBioChange = (
event: ChangeEvent<HTMLTextAreaElement>,
): void => {
updateField("bio", event.target.value);
};
const handleSubmit = (
event: FormEvent<HTMLFormElement>,
): void => {
event.preventDefault();
const validationErrors = validateForm(form);
if (Object.keys(validationErrors).length > 0) {
setErrors(validationErrors);
return;
}
// Тут можна виконати запит до сервера
console.log("Дані форми:", form);
setErrors({});
setSubmitStatus("success");
};
return (
<form onSubmit={handleSubmit} noValidate>
<div>
<label htmlFor="name">Ім'я</label>
<input
id="name"
name="name"
type="text"
value={form.name}
onChange={handleNameChange}
aria-invalid={Boolean(errors.name)}
aria-describedby={errors.name ? "name-error" : undefined}
/>
{errors.name && (
<p id="name-error" role="alert">
{errors.name}
</p>
)}
</div>
<div>
<label htmlFor="email">Електронна пошта</label>
<input
id="email"
name="email"
type="email"
value={form.email}
onChange={handleEmailChange}
aria-invalid={Boolean(errors.email)}
aria-describedby={errors.email ? "email-error" : undefined}
/>
{errors.email && (
<p id="email-error" role="alert">
{errors.email}
</p>
)}
</div>
<div>
<label htmlFor="password">Пароль</label>
<input
id="password"
name="password"
type="password"
value={form.password}
onChange={handlePasswordChange}
aria-invalid={Boolean(errors.password)}
aria-describedby={
errors.password ? "password-error" : undefined
}
/>
{errors.password && (
<p id="password-error" role="alert">
{errors.password}
</p>
)}
</div>
<div>
<label htmlFor="bio">Коротко про себе</label>
<textarea
id="bio"
name="bio"
value={form.bio}
onChange={handleBioChange}
aria-invalid={Boolean(errors.bio)}
aria-describedby={errors.bio ? "bio-error" : undefined}
/>
{errors.bio && (
<p id="bio-error" role="alert">
{errors.bio}
</p>
)}
</div>
<button type="submit">Зареєструватися</button>
{submitStatus === "success" && (
<p role="status">Форму успішно відправлено</p>
)}
</form>
);
}form містить поточні значення всіх полів.
errors містить повідомлення для полів, які не пройшли валідацію.
updateField змінює одне поле та очищає його попередню помилку.
Обробники onChange отримують типізовану подію від відповідного HTML-елемента.
handleSubmit скасовує стандартну відправку браузера.
validateForm перевіряє всі значення.
Якщо помилки є, вони відображаються біля відповідних полів.
Якщо помилок немає, дані можна передати на сервер.
Тип події залежить від HTML-елемента:
const handleInput = (
event: ChangeEvent<HTMLInputElement>,
): void => {
console.log(event.target.value);
};
const handleTextarea = (
event: ChangeEvent<HTMLTextAreaElement>,
): void => {
console.log(event.target.value);
};
const handleForm = (
event: FormEvent<HTMLFormElement>,
): void => {
event.preventDefault();
};Для <select> використовується:
ChangeEvent<HTMLSelectElement>Не варто використовувати any для всіх подій:
// Погано
const handleChange = (event: any) => {
setValue(event.target.value);
};Такий підхід вимикає перевірку типів і приховує помилки. Краще вказати конкретний тип HTML-елемента.
value та onChangeКероване поле повинно мати обидва атрибути:
<input
value={form.email}
onChange={handleEmailChange}
/>value отримує значення зі стану;
onChange оновлює стан після введення користувача.
Якщо вказати лише value, поле стане доступним тільки для читання. Якщо не вказати value, React не зможе повністю контролювати його поточне значення.
// Помилка: подія для textarea має інший тип
const handleBioChange = (
event: ChangeEvent<HTMLInputElement>,
): void => {
setForm((previousForm) => ({
...previousForm,
bio: event.target.value,
}));
};Для textarea потрібно використовувати HTMLTextAreaElement.
// Неправильно
form.email = "new@example.com";Стан потрібно змінювати через setForm.
preventDefaultconst handleSubmit = (
event: FormEvent<HTMLFormElement>,
): void => {
// Без цього браузер виконає стандартну відправку
console.log(form);
};Правильно:
const handleSubmit = (
event: FormEvent<HTMLFormElement>,
): void => {
event.preventDefault();
console.log(form);
};const updateField = (field: string, value: string): void => {
setForm((previousForm) => ({
...previousForm,
[field]: value,
}));
};Тип string дозволяє передати будь-яке ім’я поля, навіть такого, якого немає у формі. Безпечніше обмежити ключ типом keyof FormState.
Якщо користувач виправляє лише email, не потрібно приховувати помилки інших полів:
setErrors({});Краще видаляти тільки помилку поточного поля:
setErrors((previousErrors) => {
const nextErrors = { ...previousErrors };
delete nextErrors.email;
return nextErrors;
});Атрибути required, type="email" і minLength корисні, але складніші правила краще перевіряти у власній функції. Вона також потрібна перед відправленням даних на сервер.
Стан форми описуйте окремим типом TypeScript.
Для input використовуйте ChangeEvent<HTMLInputElement>.
Для textarea використовуйте ChangeEvent<HTMLTextAreaElement>.
Для відправлення форми використовуйте FormEvent<HTMLFormElement>.
Кероване поле має отримувати value зі стану та мати onChange.
Для універсального оновлення полів використовуйте keyof FormState.
Помилки зручно описувати через Partial<Record<keyof FormState, string>>.
Валідацію варто винести в окрему функцію.
Перед обробкою даних у onSubmit викликайте event.preventDefault().
Не використовуйте any і не змінюйте стан форми напряму.