React та обробка форм через Formik

React та обробка форм через Formik

  • 12 жовтня
  • читати 20 хв
Володимир Шайтан
Володимир Шайтан Senior Full Stack Developer у UKEESS Software House, Викладач Комп'ютерної школи Hillel.
Кирило Громико
Кирило Громико Frontend Developer

Багато веброзробників стикалися зі складнощами при створенні та валідації форм. Скільки труднощів виникало під час написання власних рішень! Але зараз існує безліч інструментів, які значно покращують цей процес. Сьогодні ми розглянемо декілька з них. Але спочатку детально розберімо саму проблему, для того, щоб знайти саме те рішення, яке нам підійде найкраще.

ПРОБЛЕМА

Я розіб'ю́ проблеми на блоки для більш логічного опису, оскільки їх набіжить не мало і вони не дуже прості. Отже, розбираємо проблеми нативного JavaScript:

1. Обробка стану форми — якщо ви читаєте статтю про React, то ви вже повинні були тим чи іншим чином стикатися з поняттям state і component. У нашому випадку (чистий JS) ми отримаємо проблему саме в роботі зі станом форми, тому що для керування станом форми буде потрібно написати досить багато коду. Він буде постійно відстежувати зміни й оновлювати дані на інтерфейсі. Я вже мовчу про те, що поля у формі можуть бути звʼязані між собою.

2. Валідація  тут аналогічно, треба написати досить багато коду для валідації, плюс до того нам буде потрібно писати валідаційні правила, котрі всі так люблять.

3. Обробка подій  окрім того, що робота з моделлю подій постійно вводить початківців у ступор, працюючи з чистим JS нам треба думати ще й про кількість обробників, які ми вішаємо на елементи. Від цього досить сильно залежить performance сторінки, не кажучи про те що деякі події поводяться не зовсім очевидно, а подекуди й неправильно.

4. Обробка помилок — тут все просто, в JS немає нативного способу для відображення повідомлень чи підказок юзерам, і всю цю функціональність нам треба буде писати ручками, а також тестувати.

5. Кросбраузерність — пам'ятаєте, я вище писав, що деякі події поводяться не так, як повинні. Так от, в якихось браузерах вони поводяться правильно. Залишилося тільки нормалізувати роботу у всіх браузерах, на котрих тестуємо.

6. Перевикористання коду — було б дуже добре могти перевикористовувати код, написаний для однієї форми на іншій. І так можна робити, але не використовуючи допоміжних інструментів, ви скоріше дійдете до дублювання, або напишете свою бібліотеку;)

7. Стилізація — це наше все, не забуваємо що окрім логіки яку ми пишемо, треба дати юзеру зрозуміти хоч щось. Наприклад, що він вдало заповнив форму, або що ми його не зареєструвало, бо йому ще немає 18 років.

У цьому всьому є свої плюси також, використовуючи чистий JS можна написати щось дуже гнучке і те, що буде гарно працювати в контексті саме вашого проєкту. Але це потребує часу, знань і певного рівня навичок.

Отже, всі ті аспекти що описані вище роблять роботу з формами доволі важкою та об'ємною з погляду написання коду. Саме тому, ми, розробники, так любимо використовувати допоміжні інструменти, бібліотеки та фреймворки.

Ну і як бонус, ось типовий шматок коду для обробки форми на чистому JS:

<!-- HTML: Структура форми -->

<form id="contactForm">
   <div>
       <label for="name">Ім'я:</label>
       <input type="text" id="name" name="name" />
       <span class="error" id="nameError"></span>
   </div>
   <div>
       <label for="email">Електронна пошта:</label>
       <input type="email" id="email" name="email" />
       <span class="error" id="emailError"></span>
   </div>
   <div>
       <label for="message">Повідомлення:</label>
       <textarea id="message" name="message"></textarea>
       <span class="error" id="messageError"></span>
   </div>
   <button type="submit">Відправити</button>
</form>

// JavaScript: Обробка та валідація форми
document.getElementById('contactForm').addEventListener('submit', function (event) {
   event.preventDefault(); // Відміняємо стандартну поведінку форми

   // Отримуємо значення полів
   const name = document.getElementById('name').value.trim();
   const email = document.getElementById('email').value.trim();
   const message = document.getElementById('message').value.trim();

   // Скидаємо повідомлення про помилки
   document.getElementById('nameError').textContent = '';
   document.getElementById('emailError').textContent = '';
   document.getElementById('messageError').textContent = '';

   let isValid = true;

   // Валідація імені
   if (name === '') {
       document.getElementById('nameError').textContent = 'Введіть ваше ім’я';
       isValid = false;
   }

   // Валідація електронної пошти
   const emailPattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
   if (email === '') {
       document.getElementById('emailError').textContent = 'Введіть вашу електронну пошту';
       isValid = false;
   } else if (!emailPattern.test(email)) {
       document.getElementById('emailError').textContent = 'Неправильний формат електронної пошти';
       isValid = false;
   }

   // Валідація повідомлення
   if (message === '') {
       document.getElementById('messageError').textContent = 'Введіть ваше повідомлення';
       isValid = false;
   }

   // Якщо форма пройшла валідацію
   if (isValid) {
       console.log('Форма успішно відправлена');
       console.log({ name, email, message });

       // Можна очистити поля форми або відправити дані на сервер
       document.getElementById('contactForm').reset();
   }
});

СТРИБАЄМО В REACT

React спрощує роботу з формами тим, що дозволяє легко керувати станом через useState, що забезпечує синхронізацію форми з інтерфейсом. Він також спрощує обробку подій, мінімізує проблеми з кросбраузерністю, і дозволяє повторно використовувати код компонентів. Це робить роботу з формами більш ефективною та зрозумілою.

import React, { useState } from 'react';

const SimpleForm = () => {
   const [formData, setFormData] = useState({ name: '', email: '' });

   const handleChange = (e) => {
       const { name, value } = e.target;
       setFormData({ ...formData, [name]: value });
   };

   const handleSubmit = (e) => {
       e.preventDefault();
       console.log(formData);
   };

   return (
       <form onSubmit={handleSubmit}>
           <input
               type="text"
               name="name"
               value={formData.name}
               onChange={handleChange}
               placeholder="Ім'я"
           />
           <input
               type="email"
               name="email"
               value={formData.email}
               onChange={handleChange}
               placeholder="Електронна пошта"
           />
           <button type="submit">Відправити</button>
       </form>
   );
};

export default SimpleForm;

Знаючи базу роботи з React, у цьому коді розібратися досить просто, тут головна логіка полягає в тому, що наші input елементи є керованими компонентами й рендерять тільки те що є в стейті formData, а синхронізуються зі стейтом через handleSubmit обробник.

У цьому випадку робота з готовим інструментарієм вже набагато полегшує роботу з формами, ніж у нативному JS. Але можна ще більше спростити цей процес, використовуючи розроблені під ці задачі бібліотеки, наприклад, Formik.

FORMIC — БІБЛІОТЕКА ДЛЯ РОБОТИ З ФОРМАМИ В REACT

Formik — це невелика група компонентів і хуків для створення форм у React та React Native. Він значно полегшує тестування, рефакторинг і розуміння ваших форм.

Основні функції Formik:

  • отримання значень зі стану форми та їх запис у стан форми;
  • валідація та повідомлення про помилки;
  • обробка відправки форми;
  • зменшення обсягу коду завдяки автоматизації багатьох завдань, що знижує ймовірність помилок.

Наведу базовий приклад обробки форми через Formik.

import React from 'react';
import { useFormik } from 'formik';

const SimpleFormikForm = () => {
   // Використовуємо useFormik для обробки логіки форми
   const formik = useFormik({
       initialValues: {
           name: '',
           email: '',
       },
       onSubmit: (values) => {
           alert(`Ім'я: ${values.name}, Електронна пошта: ${values.email}`);
       },
   });

   return (
       <form onSubmit={formik.handleSubmit}>
           <div>
               <label htmlFor="name">Ім'я:</label>
               <input
                   id="name"
                   name="name"
                   type="text"
                   onChange={formik.handleChange}
                   value={formik.values.name}
               />
           </div>

           <div>
               <label htmlFor="email">Електронна пошта:</label>
               <input
                   id="email"
                   name="email"
                   type="email"
                   onChange={formik.handleChange}
                   value={formik.values.email}
               />
           </div>

           <button type="submit">Відправити</button>
       </form>
   );
};

export default SimpleFormikForm;

Покроково:

1. Імпорт та ініціалізація Formik

import React from 'react';
import { useFormik } from 'formik';

2. Налаштування useFormik

const formik = useFormik({
   initialValues: {
       name: '',
       email: '',
   },
   onSubmit: (values) => {
       alert(`Ім'я: ${values.name}, Електронна пошта: ${values.email}`);
   },
});

initialValues: це початкові значення для нашої форми. У цьому випадку ми визначили два поля: name та email, і обидва мають порожні значення.

onSubmit: це функція, яка викликається, коли форма відправляється. У нашому випадку ми просто виводимо повідомлення з даними, які ввів користувач.

3. Рендеринг форми

return (
   <form onSubmit={formik.handleSubmit}>
       <div>
           <label htmlFor="name">Ім'я:</label>
           <input
               id="name"
               name="name"
               type="text"
               onChange={formik.handleChange}
               value={formik.values.name}
           />
       </div>

Ми використовуємо звичайний HTML-тег <form>, але підключаємо його до Formik через onSubmit={formik.handleSubmit}. Це гарантує, що Formik обробить відправку форми.

handleChange від Formik прив’язується до onChange кожного інпуту, щоб автоматично оновлювати значення в стані. value={formik.values.name} вказує значення інпуту на поточний стан форми.

4. Обробка другого поля

<div>
   <label htmlFor="email">Електронна пошта:</label>
   <input
       id="email"
       name="email"
       type="email"
       onChange={formik.handleChange}
       value={formik.values.email}
   />
</div>

Аналогічно, поле email підключене до Formik, використовуючи formik.handleChange та formik.values.email.

5. Кнопка відправки

<button type="submit">Відправити</button>
</form>
);

Кнопка submit викликає formik.handleSubmit, коли користувач натискає на неї, відправляючи значення форми для обробки.

YUP — БІБЛІОТЕКА ДЛЯ ВАЛІДАЦІЇ ДАНИХ

Yup — це бібліотека для валідації даних і створення схем об'єктів. Вона дозволяє легко визначати правила валідації для об'єктів і полів, використовуючи зручний та читабельний синтаксис. Yup працює чудово як самостійно, так і в поєднанні з іншими бібліотеками.

Приклад використання Yup на чистому JavaScript.

// Імпортуємо бібліотеку Yup
import * as Yup from 'yup';

// Створюємо схему валідації
const validationSchema = Yup.object().shape({
   name: Yup.string()
       .required('Ім’я є обов’язковим')
       .min(2, 'Ім’я має бути не менше 2 символів'),
   email: Yup.string()
       .required('Електронна пошта є обов’язковою')
       .email('Невірний формат електронної пошти'),
   age: Yup.number()
       .required('Вік є обов’язковим')
       .min(18, 'Вам має бути 18 років або більше')
});

// Дані, які потрібно перевірити
const formData = {
   name: 'Іван',
   email: 'ivan@example.com',
   age: 17,
};

// Валідація даних
validationSchema
   .validate(formData, { abortEarly: false }) // abortEarly: false - показує всі помилки
   .then((validData) => {
       console.log('Дані валідні:', validData);
   })
   .catch((errors) => {
       console.log('Помилки валідації:', errors.errors);
   });

Що відбувається в прикладі?

  1. Ми створили схему validationSchema з правилами валідації для полів nameemail, і age.
  2. Далі ми перевіряємо об'єкт formData за допомогою validate().
  3. Якщо дані валідні, вони будуть відображені в консолі. Якщо ні, отримаємо масив помилок.

Це дозволяє легко визначати й перевіряти правила валідації, роблячи код чистим і зручним.

YUP + FORMIC = ЧУДОВА ПАРА

Formik + Yup — це дійсно чудова пара, тому що вони просто працюють разом. Formik робить роботу з формами в React зручною, але валідація — це інша історія, і тут з'являється Yup.

Отже, чому вони так добре підходять один одному?

Formik вже знає про існування Yup, тому не потрібно займатися зайвою інтеграцією. Ви просто вказуєте validationSchema у Formik, передаєте туди схему Yup, і все — він сам подбає про валідацію.

Yup — це така штука, що дозволяє описати валідацію просто й декларативно. Жодних ручних перевірок типу «якщо порожнє — покажи помилку». Ви одразу описуєте всі правила, і вони працюють.

Formik самостійно відстежує стан форми, помилки, торкання полів — і з Yup це все працює автоматично. Вам не потрібно морочитися з тим, коли і як показувати повідомлення про помилки, Formik це бере на себе.

Якщо вам потрібно додати якісь свої правила валідації, Yup це дозволяє зробити. Тобто ця парочка дає можливість легко та швидко налаштувати валідацію, і водночас вони настільки гнучкі, що підходять для будь-яких випадків.

Тому Yup + Formik — це реально зручно, швидко, і працює відмінно майже з коробки.

Приклад нижче буде важчий за попередні, але він демонструє нетривіальну роботу самої Formik, а також реалізовує валідацію даних через Yup, що полегшує комплексну логіку компонента динамічної форми.

Головне — не поспішай і вдумливо читай код, тоді все точно стане зрозуміло.

import React from 'react';
import { useFormik, FieldArray } from 'formik';
import * as Yup from 'yup'; // Імпортуємо Yup для валідації

const DynamicFormikForm = () => {
   // Створюємо схему валідації за допомогою Yup
   const validationSchema = Yup.object().shape({
       friends: Yup.array()
           .of(
               Yup.object().shape({
                   name: Yup.string()
                       .required("Введіть ім'я"), // Перевірка на обов'язкове заповнення
                   email: Yup.string()
                       .email('Неправильний формат електронної пошти') // Перевірка формату
                       .required('Введіть електронну пошту'), // Перевірка на обов'язкове заповнення
               })
           )
           .min(1, 'Додайте хоча б одного друга'), // Перевірка, щоб був хоча б один друг
   });

   // Використовуємо useFormik для роботи з формою
   const formik = useFormik({
       initialValues: {
           friends: [{ name: '', email: '' }], // Початкове значення масиву з одним об'єктом
       },
       validationSchema, // Підключаємо нашу схему валідації Yup
       onSubmit: (values) => {
           console.log('Відправлені значення:', values); // Виводимо у консоль значення при відправці
           alert('Форма успішно відправлена!'); // Повідомлення при успішній відправці
       },
   });

   return (
       <form onSubmit={formik.handleSubmit}>
           <h2>Друзі</h2>
           <FieldArray
               name="friends"
               render={(arrayHelpers) => (
                   <div>
                       {formik.values.friends && formik.values.friends.length > 0 ? (
                           formik.values.friends.map((friend, index) => (
                               <div key={index} style={{ marginBottom: '15px' }}>
                                   <div>
                                       <label htmlFor={`friends.${index}.name`}>Ім'я:</label>
                                       <input
                                           type="text"
                                           name={`friends.${index}.name`}
                                           value={friend.name}
                                           onChange={formik.handleChange}
                                           onBlur={formik.handleBlur}
                                       />
                                       {formik.touched.friends &&
                                           formik.touched.friends[index] &&
                                           formik.errors.friends &&
                                           formik.errors.friends[index] &&
                                           formik.errors.friends[index].name && (
                                               <div className="error">
                                                   {formik.errors.friends[index].name}
                                               </div>
                                           )}
                                   </div>
                                   <div>
                                       <label htmlFor={`friends.${index}.email`}>Електронна пошта:</label>
                                       <input
                                           type="email"
                                           name={`friends.${index}.email`}
                                           value={friend.email}
                                           onChange={formik.handleChange}
                                           onBlur={formik.handleBlur}
                                       />
                                       {formik.touched.friends &&
                                           formik.touched.friends[index] &&
                                           formik.errors.friends &&
                                           formik.errors.friends[index] &&
                                           formik.errors.friends[index].email && (
                                               <div className="error">
                                                   {formik.errors.friends[index].email}
                                               </div>
                                           )}
                                   </div>
                                   <button
                                       type="button"
                                       onClick={() => arrayHelpers.remove(index)} // Видалення друга
                                   >
                                       Видалити
                                   </button>
                               </div>
                           ))
                       ) : (
                           <div>Додайте хоча б одного друга</div>
                       )}
                       <button
                           type="button"
                           onClick={() => arrayHelpers.push({ name: '', email: '' })} // Додавання нового друга
                       >
                           Додати друга
                       </button>
                   </div>
               )}
           />

           <button type="submit">Відправити</button>
       </form>
   );
};

export default DynamicFormikForm;

Пояснюю:

1. Схема валідації Yup

const validationSchema = Yup.object().shape({
   friends: Yup.array()
       .of(
           Yup.object().shape({
               name: Yup.string()
                   .required("Введіть ім'я"), // Перевірка на обов'язкове заповнення
               email: Yup.string()
                   .email('Неправильний формат електронної пошти') // Перевірка формату
                   .required('Введіть електронну пошту'), // Перевірка на обов'язкове заповнення
           })
       )
       .min(1, 'Додайте хоча б одного друга'), // Перевірка, щоб був хоча б один друг
});

Це основна магія Yup — ось, де задаються всі правила. Yup.object().shape({ ... }) дозволяє визначити структуру об'єкта. Наш friends — це масив (Yup.array()), кожен елемент якого має бути об’єктом із полями name та email. Коротше, Yup чітко описує все, що ми очікуємо побачити у формі, і ви одразу бачите всі правила в одному місці.

2. Використання useFormik із Yup

const formik = useFormik({
   initialValues: {
       friends: [{ name: '', email: '' }],
   },
   validationSchema, // Підключаємо нашу схему валідації Yup
   onSubmit: (values) => {
       console.log('Відправлені значення:', values);
       alert('Форма успішно відправлена!');
   },
});

Це вже знайомий useFormik, але зверни увагу, як ми просто підставили validationSchema. Formik тепер знає, що йому робити з даними, та більше не треба морочитися з функцією validate — Yup все зробить за нас. Ось тут і видно справжню силу їхньої «пари». Мінімум коду максимум результату.

3. Відображення помилок

{formik.touched.friends &&
formik.touched.friends[index] &&
formik.errors.friends &&
formik.errors.friends[index] &&
formik.errors.friends[index].name && (
   <div className="error">
       {formik.errors.friends[index].name}
   </div>
)}

Тут трішки заплутано, але важливо розібратися. formik.touched вказує, що користувач вже взаємодіяв з цим полем, а formik.errors містить повідомлення про помилки від Yup. Якщо всі ці умови дотримуються — ми показуємо помилку. Так, це здається громіздким, але саме така перевірка гарантує, що ми не показуємо помилку одразу, поки користувач ще не працював із полем.

4. FieldArray та динамічні поля

<FieldArray
   name="friends"
   render={(arrayHelpers) => (
       <div>
           {formik.values.friends && formik.values.friends.length > 0 ? (
               formik.values.friends.map((friend, index) => (
                   <div key={index} style={{ marginBottom: '15px' }}>
                       {/* Тут ваші поля для name і email */}
                       <button
                           type="button"
                           onClick={() => arrayHelpers.remove(index)} // Видалення друга
                       >
                           Видалити
                       </button>
                   </div>
               ))
           ) : (
               <div>Додайте хоча б одного друга</div>
           )}
           <button
               type="button"
               onClick={() => arrayHelpers.push({ name: '', email: '' })} // Додавання нового друга
           >
               Додати друга
           </button>
       </div>
   )}
/>

Це головний прикол з динамічними полями. FieldArray дозволяє працювати з масивом даних у формі. Через arrayHelpers ми можемо додавати (push), або видаляти (remove) друзів. І коли Yup каже, що має бути хоча б один друг, Formik і FieldArray допомагають нам зберігати цей стан. По суті, це означає, що будь-яка зміна в масиві «друзів»‎ миттєво відображається у формі.

ВИСНОВОК

Отже, підбиваючи підсумки. Робота з формами в чистому JavaScript — це той ще головний біль: безліч коду, складна валідація, ручна обробка помилок і купа повторень. React значно полегшує цей процес, дозволяючи тримати стан форми під контролем й обробляти події, але все одно багато рутини залишається на нас.

І тут на сцену виходить Formik — він допомагає зробити все ще простіше, взявши на себе обробку стану й відправку даних форми. У поєднанні з Yup він перетворює валідацію у щось максимально декларативне та зручне. Всю логіку валідації одразу видно, її легко підтримувати й, головне, все працює автоматично разом із Formik.

Таким чином, пара Formik + Yup дійсно полегшує життя розробника, дозволяючи зосередитися на бізнес-логіці, а не на тому, як обробити кожну дрібну помилку чи валідацію форми. У результаті ви отримуєте потужний і гнучкий інструмент, який заощаджує час та нерви. Тож якщо ви ще не спробували — саме час дати цим бібліотекам шанс.

Рекомендуємо публікації по темі