TanStack Form проти Formik: що переможе у світі React-форм у 2025
Привіт, народ! За штурвалом — ваша Юлія Яворська, фронтендер-джедай зі Skillzzy! Провела чимало годин у вирі коду, намагаючись змусити інтерфейси користувача бути не лише функціональними, але й спритними. Свою кар’єру в IT почала доволі недавно, два роки тому, але зацікавленість розробкою була ще з 2018 року. Я — свого роду «світчер», оскільки окрім розробки вже близько 8 років займаюся онлайн-маркетингом (Google Ads), проєктним менеджментом та керую командами.
Сьогодні я хочу запросити вас у невеличку, сподіваюся, пізнавальну подорож у світ React-форм. Ми зазирнемо під капот двох популярних гравців на цій арені: Formik, що вже встиг стати своєрідним стандартом, та його більш молодого, але амбітного конкурента — TanStack Form. Можливо, ви вже знайомі з Formik, використовували його у своїх проєктах і цінуєте за простоту. Але чи замислювалися ви про те, що за цією зручністю можуть ховатися неочевидні пастки продуктивності?
Тож наповнюйте кубки кавою (або чим ви там підтримуєте свій кодерський дух 😉) та погнали! Розберемося разом, хто ж переможе у цій битві титанів форм у 2025 році!
Formik: «класика жанру» чи «тормоз» прогресу
Коли мова заходить про React-форми, Formik нерідко спадає на думку першим. Проте за всією його простотою ховається потенційна проблема, яка може стати критичною з часом — надмірні перерендери, що виникають при кожній зміні поля.
Як наслідок відбувається затримка інтерфейсу, що вбиває user experience and user satisfaction. А як ми всі знаємо, наші користувачі не полюбляють чекати, отож потрібно вирішувати це питання та «take an even deeper dive into», як кажуть класики.
Налаштування та конфігурація
Отож почнемо з налаштування обох бібліотек, яке є доволі легке та інтуїтивне:
- Formik
- npm install formik —save or yarn add formik
- Tanstack | React-приклад
- npm i @tanstack/react-form
- yarn add @tanstack/react-form
Пропоную заглянути додатково до офіційної документації на випадок зміни налаштувань.
Розпаковка «боксів»: Formik vs TanStack — хто швидше «заведеться»
Продовжуючи тему продуктивності варто зазначити, що розмір залежностей має безпосередній вплив на швидкість роботи та завантаження вебзастосунків. Маю на увазі, що розмір бандла є важливим фактором.
TanStack Form очевидно демонструє значно менший розмір бандла порівняно з Formik, однак точні цифри можуть варіюватися залежно від версії та конфігурації. Це означає менший обсяг JavaScript, який необхідно завантажити браузеру, що позитивно впливає на час першого відображення сторінки та загальну продуктивність застосунку.
На проєкті Skillzzy вибір легкої та ефективної бібліотеки став для нас пріоритетом.
Під час розробки модальних вікон, що містили форми, наша команда зіткнулася з проблемою частих та надмірних перерендерів компонентів навіть при незначних змінах у полях. Це суттєво впливає на продуктивність та перформанс платформи. Після ретельного аналізу, або як кажуть програмісти, «research», було виявлено особливість внутрішньої реалізації Formik.
Formik за замовчуванням схильний порівнювати об’єкт значень форми при кожній зміні поля, навіть якщо зміни були мінімальними. Цей процес глибокого порівняння (deep comparison*) може бути обчислювально витратним, особливо для великих і складних форм, що і призводило до непотрібних перерендерів компонентів, які фактично не залежали від змінених даних.
*Хоча прямого твердження на кшталт «Formik завжди глибоко порівнює» немає, однак те, як Formik відстежує зміни та ініціює оновлення, натякає на це.
Звичайно, є способи вирішення цієї проблеми, такі як хук React.memo. Однак такий підхід часто призводить до нагромадження значної кількості додаткового коду та ускладнює підтримку форми. Крім того, він вимагає від розробника постійного контролю за тим, які саме частини форми потрібно оновлювати.
Міграція з Formik на TanStack: hardcore чи «легка прогулянка»
На противагу TanStack Form пропонує фундаментально інший підхід. Основні переваги TanStack Form:
- Використання неконтрольованих компонентів (за замовчуванням) з прямим зв’язуванням DOM → менше повторних рендерів. DOM сам зберігає вхідне значення (як у звичайному HTML). React зчитує значення тільки тоді, коли це необхідно (наприклад, onSubmit).
- Очевидно менший розмір банду. ~9kB (vs Formik’s ~13kB).
- Гнучка валідація. Бездоганно працює з валідацією Zod, Yup або користувацькою валідацією. Підтримує перевірку як на рівні полів, так і на рівні всієї форми (з використанням схем).
- Відсутність залежностей. Framework-agnostic
TanStack — універсальний солдат?
Вище згадувала про agnostic-підхід і пропоную в цій частині розглянути, що це таке. Якщо коротко, то дослівно «незалежний від будь-яких фреймворків». Така незалежність досягається завдяки тому, що TanStack Form не використовує специфічні для певного фреймворку API або концепції у своєму основному коді.
Але виникає логічне питання, як тоді він інтегрується з конкретними фреймворками? Якщо думати логічно, з’являється ідея, що «мають же бути якісь механізми, які адаптують системи одна до іншої».
І тут в гру входить такий патерн програмування як адаптери. Не чули?! Не страшно, я теж відкрила цей патерн за підказкою мого викладача та ментора Володимир Шайтан, людини з майже десятьма роками досвіду в розробці та технологіях.
Адаптер, ти хто такий?
Візьмемо пояснення з refactoring.guru. Адаптер — це структурний патерн проєктування, що дає змогу об’єктам із несумісними інтерфейсами працювати разом. Це об’єкт-перекладач, який трансформує інтерфейс або дані одного об’єкта таким чином, щоб він став зрозумілим іншому об’єкту. Адаптер загортає один з об’єктів так, що інший об’єкт навіть не підозрює про існування першого.
До прикладу, для React адаптер надає React Hooks, такі як useForm або useField тощо. Ці хуки інкапсулюють логіку TanStack Form і роблять її доступною у функціональних компонентах React, використовуючи реактивний стан та механізми рендерингу React. Приклад використання можна побачити в самому коді нижче.
Аналогічно для Vue існує окремий адаптер, який надає композиційні функції (Composition API) або опції (Options API) для інтеграції з реактивною системою Vue. Серед них useForm, useField, тощо.
Реальний кейс: «перевзуваємо» Formik на TanStack на льоту
А тепер пропоную розглянути процес переходу з Formik на Tanstack Form на прикладі нашого проєкту.
Tan Stack Form — FeedbackProjectModal.jsx
import { Typography } from '@mui/material'; import { useTranslation } from 'react-i18next'; import { useForm } from '@tanstack/react-form'; import { useStore } from '@tanstack/react-form'; import { useSelector } from 'react-redux'; import { useSnackbar } from 'notistack'; import { useCreateFeedbackMutation } from '@redux/api/slices/feedbackProjectModalApiSlice.js'; import ButtonDef from '@components/FormsComponents/Buttons/ButtonDef'; import FormSelect from '@components/FormsComponents/Inputs/FormSelect'; import TextAreaInput from '@components/FormsComponents/Inputs/TextAreaInput'; import { FeedbackProjectModalSchema } from '@utils/validationSchemas/index'; import { modalNames } from '@utils/constants/modalNames.js'; import { useModalController } from '@utils/hooks/useModalController.js'; import { feedbackOptions } from './constants'; import { styles } from './FeedbackProjectModal.styles'; const FeedbackProjectModal = () => { const { t } = useTranslation(); const { enqueueSnackbar } = useSnackbar(); const { id } = useSelector((state) => state.auth.user.data); const { closeModal } = useModalController(); const [createFeedback, { isLoading }] = useCreateFeedbackMutation(); const from = useForm({ defaultValues: { select: '', feedbackText: '', }, validators: { onChange: FeedbackProjectModalSchema }, onSubmit: async ({ value }) => { try { await createFeedback({ userId: id, type: value.select, text: value.feedbackText, }); enqueueSnackbar(t('modal.feedbackProjectModal.success'), { variant: 'success' }); } catch (error) { enqueueSnackbar(t('modal.feedbackProjectModal.error_429'), { variant: 'error' }); } closeModal(modalNames.feedbackProjectModal); }, }); const isDirty = useStore(form.store, (state) => state.isDirty); const isValid = useStore(form.store, (state) => state.isValid); return ( <> <Typography sx={styles.title} variant='h6'> {t('modal.feedbackProjectModal.title')} </Typography> <form onSubmit={(e) => { e.preventDefault(); form.handleSubmit(); }} > <form.Field name='select'> {(field) => ( <FormSelect isTranslated required countries={feedbackOptions} error={field.state?.meta?.errors?.length > 0} handleBlur={field.handleBlur} handleChange={(e) => { field.handleChange(e.target.value); }} helperText={t(field.state?.meta?.errors[0]?.message)} label={t('modal.feedbackProjectModal.formSelectLabel')} name={field.name} value={field.state.value} variant='outlined' /> )} </form.Field> <form.Field name='feedbackText'> {(field) => ( <TextAreaInput required error={field.state?.meta?.errors?.length > 0} handleBlur={field.handleBlur} handleChange={(e) => { field.handleChange(e.target.value); }} helperText={t(field.state?.meta?.errors[0]?.message)} label={t('modal.feedbackProjectModal.textAreaLabel')} name={field.name} placeholder={t('modal.feedbackProjectModal.textPlaceholder')} value={field.state.value} /> )} </form.Field> <ButtonDef fullWidth disabled={!isDirty || !isValid} label={t('modal.feedbackProjectModal.button')} loading={isLoading} sx={styles.btn} type='submit' variant='contained' /> </form> </> ); }; export default FeedbackProjectModal;
На що варто звернути увагу:
1. Імпорт useForm та useStore з @tanstack/react-form. Замість імпорту useFormik з Formik імпортуємо хук useForm та хук для реактивності userStore з бібліотеки Tanstack Form.
import { useForm } from '@tanstack/react-form'; import { useStore } from '@tanstack/react-form';
2. Ініціалізація форми за допомогою useForm. Замість використання хука useFormik ініціалізуємо форму за допомогою хука useForm від Tanstack Form. Цей хук приймає об’єкт конфігурації, схожий на useFormik, але з деякими відмінностями в структурі та можливостях.
const from = useForm({ defaultValues: { select: '', feedbackText: '', }, validators: { onChange: FeedbackProjectModalSchema }, onSubmit: async ({ value }) => { // ... код обробки відправки форми }, });
- default Values. Схожий принцип, визначає початкові значення полів форми.
- validators. Визначає схему валідації. У Tanstack Form валідація може бути визначена як на подію onChange, так і на onSubmit.
- onSubmit. Функція, яка викликається при успішній відправці форми. Зверніть увагу, що значення форми передаються в об’єкті value.
3. Використання form.Field для реєстрації полів. Замість компонента Field або ручного підключення onChange, onBlur та value до кожного поля, Tanstack Form використовує компонент form.Field. Цей компонент приймає пропс name (назву поля) і рендер-пропс (render prop) або children у вигляді функції. Ця функція отримує об’єкт field, який містить необхідні обробники та стан поля.
<form.Field name='select'> {(field) => ( <FormSelect isTranslated required countries={feedbackOptions} error={field.state?.meta?.errors?.length > 0} handleBlur={field.handleBlur} handleChange={(e) => { field.handleChange(e.target.value); }} helperText={t(field.state?.meta?.errors[0]?.message)} label={t('modal.feedbackProjectModal.formSelectLabel')} name={field.name} value={field.state.value} variant='outlined' /> )} </form.Field> <form.Field name='feedbackText'> {(field) => ( <TextAreaInput required error={field.state?.meta?.errors?.length > 0} handleBlur={field.handleBlur} handleChange={(e) => { field.handleChange(e.target.value); }} helperText={t(field.state?.meta?.errors[0]?.message)} label={t('modal.feedbackProjectModal.textAreaLabel')} name={field.name} placeholder={t('modal.feedbackProjectModal.textPlaceholder')} value={field.state.value} /> )} </form.Field>
4. Обробка відправки форми за допомогою form.handleSubmit. Замість виклику formik.handleSubmit, використовуємо метод form.handleSubmit об’єкта form, отриманого від useForm.
<form onSubmit={(e) => { e.preventDefault(); form.handleSubmit(); }} > {/* ... */} </form>
5. Реактивність за допомогою useStore. Tanstack Form надає гнучкі механізми для підписки на зміни стану форми та окремих полів, що дозволяє оптимізувати рендеринг. У цьому прикладі використовується хук useStore для отримання реактивних значень isDirty (чи була змінена хоча б одне поле) та isValid (чи валідна форма).
const isDirty = useStore(form.store, (state) => state.isDirty); const isValid = useStore(form.store, (state) => state.isValid); <ButtonDef ... disabled={!isDirty || !isValid} ... /> </form> </>
- useStore(form.store, selector) приймає сховище форми (form.store) та селектор-функцію. Селектор-функція отримує поточний стан сховища і повертає значення, на яке потрібно підписатися. Компонент буде перерендерено тільки тоді, коли значення, що повертається селектором, зміниться.
Formik — FeedbackProjectModal.jsx
import { Typography } from '@mui/material'; import { useTranslation } from 'react-i18next'; import { useFormik } from 'formik'; import { useSelector } from 'react-redux'; import { useSnackbar } from 'notistack'; import { useCreateFeedbackMutation } from '@redux/api/slices/feedbackProjectModalApiSlice.js'; import ButtonDef from '@components/FormsComponents/Buttons/ButtonDef'; import FormSelect from '@components/FormsComponents/Inputs/FormSelect'; import TextAreaInput from '@components/FormsComponents/Inputs/TextAreaInput'; import { FeedbackProjectModalSchema } from '@utils/validationSchemas/index'; import { modalNames } from '@utils/constants/modalNames.js'; import { useModalController } from '@utils/hooks/useModalController.js'; import { feedbackOptions } from './constants'; import { styles } from './FeedbackProjectModal.styles'; const initialValues = { select: '', feedbackText: '', }; const FeedbackProjectModal = () => { const { t } = useTranslation(); const { enqueueSnackbar } = useSnackbar(); const { id } = useSelector((state) => state.auth.user.data); const { closeModal } = useModalController(); const [createFeedback, { isLoading }] = useCreateFeedbackMutation(); const onSubmit = async (values) => { try { await createFeedback({ userId: id, type: values.select, text: values.feedbackText, }); enqueueSnackbar(t('modal.feedbackProjectModal.success'), { variant: 'success' }); } catch (error) { enqueueSnackbar(t('modal.feedbackProjectModal.error_429'), { variant: 'error' }); } closeModal(modalNames.feedbackProjectModal); }; const formik = useFormik({ initialValues, validationSchema: FeedbackProjectModalSchema, onSubmit, }); return ( <> <Typography sx={styles.title} variant='h6'> {t('modal.feedbackProjectModal.title')} </Typography> <form onSubmit={formik.handleSubmit}> <FormSelect isTranslated required countries={feedbackOptions} handleBlur={formik.handleBlur} handleChange={formik.handleChange} label={t('modal.feedbackProjectModal.formSelectLabel')} name='select' value={formik.values.select} variant='outlined' /> <TextAreaInput required handleBlur={formik.handleBlur} handleChange={formik.handleChange} label={t('modal.feedbackProjectModal.textAreaLabel')} name='feedbackText' placeholder={t('modal.feedbackProjectModal.textPlaceholder')} value={formik.values.feedbackText} /> <ButtonDef fullWidth disabled={!formik.isValid || !formik.dirty || isLoading} label={t('modal.feedbackProjectModal.button')} loading={isLoading} sx={styles.btn} type='submit' variant='contained' /> </form> </> ); }; export default FeedbackProjectModal;
Фінальний раунд: хто ж забере корону у 2025
Хоча на момент написання статті бракує прямих, кількісних бенчмарків, які б порівнювали продуктивність TanStack Form та Formik у ідентичних сценаріях, існуючі дані та архітектурні відмінності (менший розмір бандла TanStack Form, використання неконтрольованих компонентів за замовчуванням, framework agnostic) дають валідні підстави для певних висновків.
А саме: TanStack Form має значний потенціал стати серйозним конкурентом Formik у 2025 році та навіть перевершити його в певних аспектах, що і підтвердив наш практичний досвід 🚀
Звісно, Formik залишається потужною та добре за рекомендованою бібліотекою з великою спільнотою, і, незважаючи на те, що новіші рішення, такі як TanStack Form, набирають популярності, Formik все ще є стандартом у багатьох великих компаніях. Через складність міграції Formik все ще зберігає свою поширеність, що робить навички роботи з ним важливими для розробників. Однак якщо ви стикаєтеся з проблемами продуктивності у ваших формах або шукаєте більш сучасний та гнучкий інструмент, TanStack Form безумовно заслуговує на увагу та може стати вдалим вибором для вашого наступного проєкту або для рефакторингу існуючих.
Сподіваємося, цей практичний приклад та огляд допоможуть прийняти обґрунтоване рішення щодо використання TanStack Form у вашій розробці. Не бійтеся експериментувати та використовувати інструменти, які найкраще відповідають вашим потребам!
З любов’ю (і парою задебажених годин), ваш Front-End Developer, який нарешті розібрався з формами 😉
Чекаю на Ваші коментарі та фідбеки.
20 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів