Formik vs Final Form: So sánh các thư viện Form trong React (React Roadmap)

Chào mừng các bạn quay trở lại với series “React Roadmap – Lộ trình học React 2025“! Sau khi đã cùng nhau tìm hiểu về những kiến thức nền tảng của React như React là gì, Class vs Functional Components, JSX, Props vs State, useState và useEffect, và gần đây nhất là cách xử lý form gọn gàng với React Hook Form, hôm nay chúng ta sẽ tiếp tục khám phá hai cái tên phổ biến khác trong thế giới quản lý form phức tạp: Formik và Final Form.

Form là một phần không thể thiếu trong hầu hết các ứng dụng web. Từ form đăng nhập, đăng ký đến form gửi dữ liệu phức tạp, chúng ta đều phải làm việc với chúng. Tuy nhiên, việc quản lý state của từng input, xử lý sự kiện thay đổi (onChange), blur (onBlur), validate dữ liệu, hiển thị thông báo lỗi, và xử lý submit có thể trở nên khá lộn xộn và tốn công sức khi làm thủ công chỉ với useStatexử lý sự kiện truyền thống.

Đây chính là lúc các thư viện quản lý form phát huy tác dụng. Chúng cung cấp một cấu trúc và API giúp chúng ta tập trung vào logic nghiệp vụ thay vì sa lầy vào các chi tiết kỹ thuật lặp đi lặp lại. Formik và Final Form là hai trong số những thư viện đi đầu trong lĩnh vực này (bên cạnh React Hook Form đã được giới thiệu). Mặc dù cùng giải quyết vấn đề quản lý form, cách tiếp cận và triết lý thiết kế của chúng lại khá khác biệt. Bài viết này sẽ đi sâu vào so sánh hai thư viện này để giúp bạn, đặc biệt là các bạn mới làm quen với React, hiểu rõ hơn và đưa ra lựa chọn phù hợp cho dự án của mình.

Tại sao cần dùng thư viện Form trong React?

Trước khi đi sâu vào Formik và Final Form, hãy cùng nhắc lại một chút về những thách thức khi xử lý form trong React mà các thư viện này giải quyết:

  1. Quản lý State tập trung: Mỗi input cần có state riêng (giá trị hiện tại). Với form có nhiều trường, việc quản lý state này trong component gốc hoặc một custom hook có thể trở nên cồng kềnh.
  2. Xử lý sự kiện thay đổi và Blur: Cần lắng nghe sự kiện onChange để cập nhật state và onBlur để xác định trường nào đã được người dùng chạm vào (touched) cho mục đích validation.
  3. Validation: Kiểm tra tính hợp lệ của dữ liệu nhập vào là rất quan trọng. Việc thực hiện validation tức thời (khi gõ), sau khi blur, hoặc khi submit, hiển thị lỗi tương ứng cho từng trường đòi hỏi nhiều logic điều kiện (Conditional Rendering).
  4. Theo dõi trạng thái Form: Các trạng thái như isSubmitting (đang submit), isValid (form có hợp lệ không), isDirty (form đã bị thay đổi chưa), touched (trường nào đã bị chạm vào) cần được theo dõi để điều khiển giao diện và hành vi (ví dụ: disable nút submit).
  5. Xử lý Submit: Thu thập dữ liệu từ các trường, thực hiện validation cuối cùng, và gọi API hoặc hàm xử lý khi form được submit.
  6. Hiệu suất: Với mỗi lần gõ phím, state của input thay đổi, gây re-render component chứa form. Với form lớn, điều này có thể ảnh hưởng đến hiệu suất.

Các thư viện quản lý form giúp bạn tập trung hóa và trừu tượng hóa các tác vụ này, cung cấp API rõ ràng và hiệu quả để xử lý toàn bộ vòng đời của một form.

Formik: “Hoàn thành công việc” một cách nhanh chóng

Formik, được tạo bởi Jared Palmer, là một thư viện rất phổ biến và được sử dụng rộng rãi trong cộng đồng React. Triết lý của Formik là cung cấp một giải pháp toàn diện để quản lý state của form, validation, và submit trong một “bao bì” (container) duy nhất. Nó quản lý state form *nội bộ* bên trong context hoặc thông qua hook.

Formik cung cấp một số cách để sử dụng:

  1. useFormik Hook: Cách phổ biến nhất trong các ứng dụng hiện đại sử dụng functional components và Hooks. Bạn gọi hook này trong component, nó trả về một object chứa các giá trị form (values), lỗi (errors), trạng thái chạm (touched), các hàm xử lý sự kiện (handleChange, handleBlur), và hàm submit (handleSubmit).
  2. <Formik> Component (Render Props hoặc Children-as-a-function): Cách truyền thống hơn, bạn bọc form của mình trong component <Formik>. Component này sử dụng pattern Render Props (hoặc children là một hàm) để cung cấp các giá trị và hàm tương tự như useFormik cho component con của bạn.

Formik làm việc rất tốt với các thư viện validation schema như Yup hoặc Zod, giúp định nghĩa schema validation một cách rõ ràng và tái sử dụng.

Ví dụ đơn giản với Formik (sử dụng useFormik Hook):

Hãy xem cách tạo một form đăng ký đơn giản với validation sử dụng useFormik:


import React from 'react';
import { useFormik } from 'formik';
import * as Yup from 'yup'; // Thường dùng Yup để validation schema

const SignupFormFormik = () => {
  // Định nghĩa schema validation với Yup
  const validationSchema = Yup.object({
    firstName: Yup.string()
      .max(15, 'Phải dài tối đa 15 ký tự')
      .required('Bắt buộc'),
    lastName: Yup.string()
      .max(20, 'Phải dài tối đa 20 ký tự')
      .required('Bắt buộc'),
    email: Yup.string()
      .email('Địa chỉ email không hợp lệ')
      .required('Bắt buộc'),
  });

  const formik = useFormik({
    initialValues: { // Giá trị khởi tạo cho form
      firstName: '',
      lastName: '',
      email: '',
    },
    validationSchema: validationSchema, // Gắn schema validation
    onSubmit: values => { // Hàm xử lý khi form hợp lệ và được submit
      alert(JSON.stringify(values, null, 2));
      // Thường là nơi gọi API để gửi dữ liệu
    },
  });

  return (
    <form onSubmit={formik.handleSubmit}>
      <div>
        <label htmlFor="firstName">Tên</label>
        <input
          id="firstName"
          name="firstName"
          type="text"
          onChange={formik.handleChange} // Cập nhật giá trị khi gõ
          onBlur={formik.handleBlur}     // Đánh dấu trường đã chạm khi blur
          value={formik.values.firstName} // Bind giá trị từ state của formik
        />
        {/* Hiển thị lỗi nếu trường đã chạm và có lỗi */}
        {formik.touched.firstName && formik.errors.firstName ? (
          <div style={{ color: 'red' }}>{formik.errors.firstName}</div>
        ) : null}
      </div>

      <div>
        <label htmlFor="lastName">Họ</label>
        <input
          id="lastName"
          name="lastName"
          type="text"
          onChange={formik.handleChange}
          onBlur={formik.handleBlur}
          value={formik.values.lastName}
        />
        {formik.touched.lastName && formik.errors.lastName ? (
          <div style={{ color: 'red' }}>{formik.errors.lastName}</div>
        ) : null}
      </div>

      <div>
        <label htmlFor="email">Email</label>
        <input
          id="email"
          name="email"
          type="email"
          onChange={formik.handleChange}
          onBlur={formik.handleBlur}
          value={formik.values.email}
        />
        {formik.touched.email && formik.errors.email ? (
          <div style={{ color: 'red' }}>{formik.errors.email}</div>
        ) : null}
      </div>

      <button type="submit" disabled={formik.isSubmitting}>
        Đăng ký
      </button>
    </form>
  );
};

export default SignupFormFormik;

Như bạn thấy, Formik cung cấp các helper (handleChange, handleBlur, etc.) giúp việc binding input với state formik và xử lý validation trở nên rất trực quan.

Final Form: “Không phụ thuộc vào State”

Final Form, được tạo bởi Erik Rasmussen (tác giả của Redux-Form), có một triết lý khác biệt đáng kể. Thay vì quản lý toàn bộ state của form một cách tập trung, Final Form tập trung vào việc quản lý *quá trình* (lifecycle) của form: khi nào validate, khi nào submit, trường nào bị thay đổi, v.v. Nó được thiết kế để “không phụ thuộc vào state” (state-agnostic), nghĩa là nó không quan tâm state form của bạn được lưu trữ ở đâu (trong component state, Redux, Context API, hay bất cứ đâu). Thư viện react-final-form là binding của Final Form cho React.

Final Form sử dụng mạnh mẽ pattern Render Props thông qua các component chính:

  1. <Form> Component: Bọc toàn bộ form của bạn. Nó cung cấp các hàm và state liên quan đến toàn bộ form (như handleSubmit, submitting, valid) thông qua hàm children (Render Props).
  2. <Field> Component: Bọc từng input riêng lẻ. Đây là điểm khác biệt lớn. Component <Field> chỉ re-render khi state *của trường đó* hoặc state toàn cục *mà nó đang theo dõi* thay đổi. Điều này giúp tối ưu hóa hiệu suất bằng cách tránh re-render toàn bộ form khi chỉ một input thay đổi. Nó cung cấp các giá trị và hàm liên quan đến trường cụ thể (như input.value, meta.touched, meta.error) thông qua hàm children.

Final Form cho phép bạn xác định các hàm validation riêng lẻ cho từng trường hoặc một hàm validation chung cho cả form.

Ví dụ đơn giản với Final Form:

Đây là cách triển khai form đăng ký tương tự sử dụng react-final-form:


import React from 'react';
import { Form, Field } from 'react-final-form';

// Hàm xử lý khi form hợp lệ và submit
const onSubmit = async values => {
  alert(JSON.stringify(values, null, 2));
  // Gửi dữ liệu
};

// Hàm validation chung cho toàn bộ form
const validate = values => {
  const errors = {};
  if (!values.firstName) {
    errors.firstName = 'Bắt buộc';
  } else if (values.firstName.length > 15) {
     errors.firstName = 'Phải dài tối đa 15 ký tự';
  }

  if (!values.lastName) {
    errors.lastName = 'Bắt buộc';
  } else if (values.lastName.length > 20) {
     errors.lastName = 'Phải dài tối đa 20 ký tự';
  }

  if (!values.email) {
    errors.email = 'Bắt buộc';
  } else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(values.email)) {
    errors.email = 'Địa chỉ email không hợp lệ';
  }

  return errors;
};

const SignupFormFinalForm = () => (
  <Form
    onSubmit={onSubmit}
    validate={validate} // Gắn hàm validation
    initialValues={{ firstName: '', lastName: '', email: '' }} // Giá trị khởi tạo
  >
    {/* Hàm children của component Form cung cấp các props liên quan đến form */}
    {({ handleSubmit, submitting, pristine }) => (
      <form onSubmit={handleSubmit}>
        <div>
          <label>Tên</label>
          {/* Component Field bọc từng input */}
          <Field
            name="firstName" // Tên trường, map với key trong values
            component="input" // Loại phần tử HTML hoặc component tùy chỉnh
            type="text"
            placeholder="Tên"
          />
          {/* Một Field khác để hiển thị lỗi, chỉ subscribe (theo dõi) các trạng thái cần thiết */}
          <Field name="firstName" subscription={{ error: true, touched: true }}>
            {({ meta: { error, touched } }) => // Hàm children của Field cung cấp props của trường đó
              touched && error ? <span style={{ color: 'red' }}>{error}</span> : null
            }
          </Field>
        </div>

        <div>
          <label>Họ</label>
          <Field
            name="lastName"
            component="input"
            type="text"
            placeholder="Họ"
          />
          <Field name="lastName" subscription={{ error: true, touched: true }}>
            {({ meta: { error, touched } }) =>
              touched && error ? <span style={{ color: 'red' }}>{error}</span> : null
            }
          </Field>
        </div>

        <div>
          <label>Email</label>
          <Field
            name="email"
            component="input"
            type="email"
            placeholder="Email"
          />
          <Field name="email" subscription={{ error: true, touched: true }}>
            {({ meta: { error, touched } }) =>
              touched && error ? <span style={{ color: 'red' }}>{error}</span> : null
            }
          </Field>
        </div>

        <button type="submit" disabled={submitting || pristine}>
          Đăng ký
        </button>
      </form>
    )}
  </Form>
);

export default SignupFormFinalForm;

API của Final Form thiên về sử dụng component với Render Props hơn. Điểm đặc trưng là bạn có thể chỉ định rõ ràng trường <Field> nào cần “subscribe” (theo dõi) những trạng thái nào của form (ví dụ: chỉ cần errortouched) để tối ưu hóa re-render.

So Sánh Trực Tiếp: Formik vs Final Form

Giờ chúng ta đã xem qua cách hoạt động cơ bản của cả hai, hãy đặt chúng cạnh nhau để thấy rõ sự khác biệt.

1. Triết lý và Kiến trúc

  • Formik: Dựa trên ý tưởng một container quản lý toàn bộ state của form (values, errors, touched, v.v.) và cung cấp các helper để tương tác với state đó. Nó hoạt động giống như một bộ quản lý state mini cho form.
  • Final Form: Triết lý “không phụ thuộc vào state” và tập trung vào việc quản lý lifecycle và logic của form. Nó không tự quản lý state form của bạn mà hoạt động dựa trên mô hình Pub/Sub (Publisher/Subscriber), nơi các trường đăng ký lắng nghe các thay đổi cụ thể mà chúng quan tâm.

2. API

3. Hiệu suất (Re-renders)

  • Formik: Theo mặc định, khi state của formik thay đổi (ví dụ: gõ vào một input), component sử dụng useFormik hoặc component con của <Formik> sẽ re-render. Đối với form đơn giản, điều này không đáng kể. Với form phức tạp có nhiều trường hoặc các component con nặng, việc re-render toàn bộ form có thể ảnh hưởng đến hiệu suất. Formik cung cấp các cách tối ưu (như sử dụng React.memo hoặc shouldComponentUpdate nếu dùng class component) nhưng cần cấu hình thủ công.
  • Final Form: Nhờ mô hình Pub/Sub và component <Field> chỉ subscribe vào các trạng thái cần thiết, Final Form thường ít gây re-render hơn theo mặc định. Chỉ những component <Field> hoặc component con của <Form> *thực sự* cần dữ liệu thay đổi mới re-render. Điều này làm cho Final Form có lợi thế về hiệu suất cho các form rất lớn hoặc các form có nhiều component phức tạp bên trong.

4. Validation

  • Formik: Hỗ trợ validation đồng bộ (synchronous) và bất đồng bộ (asynchronous). Rất phổ biến khi kết hợp với Yup hoặc Zod để định nghĩa validation schema tập trung.
  • Final Form: Cũng hỗ trợ validation đồng bộ và bất đồng bộ. Thường sử dụng các hàm validation đơn giản hoặc kết hợp với các thư viện validation khác theo cách thủ công hơn so với Yup trong Formik. Cung cấp validation ở cấp độ trường và cấp độ form.

5. Độ dễ học

  • Formik: Nhiều developer mới bắt đầu với Formik thường thấy dễ tiếp cận hơn, đặc biệt là với API Hook useFormik, vì nó cảm giác gần gũi với cách quản lý state và sự kiện thông thường trong React.
  • Final Form: Mô hình dựa trên Render Props và khái niệm Subscription có thể mất một chút thời gian để hiểu rõ hơn. Tuy nhiên, khi đã quen, sự linh hoạt của nó lại là một điểm cộng.

6. Kích thước Bundle (Bundle Size)

  • Formik: Core library có kích thước trung bình (khoảng 15-20KB gzipped). Khi dùng kèm Yup hoặc các addon khác, kích thước tổng sẽ tăng lên.
  • Final Form: Core Final Form library rất nhỏ (khoảng 5KB gzipped). Binding cho React (react-final-form) thêm khoảng 10KB nữa. Tổng kích thước thường là trung bình, có thể hơi nhỏ hơn Formik trong một số trường hợp, nhưng không phải là khác biệt quá lớn đối với hầu hết các ứng dụng.

7. Hệ sinh thái và Cộng đồng

  • Formik: Có một cộng đồng lớn, nhiều ví dụ, tutorial và các tích hợp sẵn có (ví dụ: bindings cho các thư viện UI như Material UI, Ant Design). Là một lựa chọn “an toàn” vì sự phổ biến.
  • Final Form: Cũng là một thư viện trưởng thành và đáng tin cậy, với một cộng đồng người dùng tích cực. Tuy nhiên, số lượng ví dụ hoặc tích hợp sẵn có thể không nhiều bằng Formik, mặc dù core library của nó rất mạnh mẽ và linh hoạt.

Bảng So Sánh Tổng Quan

Để tóm tắt, đây là bảng so sánh các điểm chính giữa Formik và Final Form:

Tính năng Formik Final Form
Ý tưởng cốt lõi Quản lý state form tập trung Quản lý logic & lifecycle, không phụ thuộc state
API chính useFormik Hook, component <Formik> (Render Props/Children) Component <Form>, <Field> (Render Props)
Hiệu suất (Re-renders) Có thể re-render toàn bộ form component nếu không tối ưu thủ công Tối ưu re-render theo mặc định (Selective Subscription của <Field>)
Validation Thường dùng kết hợp Yup, Zod; Hỗ trợ validation nội bộ Dùng hàm validation, linh hoạt kết hợp bên ngoài, validation cấp trường/form
Độ dễ học Dễ tiếp cận ban đầu với Hook, API quen thuộc Cần thời gian hiểu Render Props & mô hình Subscription
Kích thước Bundle (Gzipped) Trung bình (~15-20KB Formik core) Trung bình (Final Form core ~5KB, react-final-form ~10KB). Core nhỏ hơn, nhưng react binding cần thiết.
Hệ sinh thái Lớn, nhiều ví dụ, tích hợp sẵn Trưởng thành, tập trung vào core library, các addon riêng lẻ

Khi nào chọn Formik, khi nào chọn Final Form?

Việc lựa chọn giữa Formik và Final Form (cũng như React Hook Form) phụ thuộc vào nhiều yếu tố của dự án và team của bạn:

  • Chọn Formik nếu:
    • Bạn hoặc team của bạn ưu tiên API Hook và thấy nó trực quan, dễ sử dụng hơn.
    • Bạn cần một giải pháp “có pin đi kèm” (batteries included) cho việc quản lý state form.
    • Bạn muốn tích hợp chặt chẽ với các thư viện validation schema như Yup hoặc Zod.
    • Hầu hết các form trong ứng dụng của bạn có quy mô trung bình và bạn không quá lo lắng về hiệu suất re-render trong các trường hợp cực đoan (mặc dù bạn vẫn nên lưu ý tối ưu).
    • Bạn muốn tận dụng lợi thế của một cộng đồng lớn và nhiều tài nguyên sẵn có.
  • Chọn Final Form nếu:
    • Hiệu suất là ưu tiên hàng đầu của bạn, đặc biệt với các form rất lớn, phức tạp hoặc render các component “nặng” bên trong. Mô hình Subscription của Final Form giúp tối ưu re-render out-of-the-box.
    • Bạn thích hoặc đã quen thuộc với pattern Render Props.
    • Bạn muốn một thư viện linh hoạt hơn trong việc quản lý state (ví dụ: bạn đã có hệ thống quản lý state riêng và muốn form library hoạt động dựa trên đó).
    • Bạn đánh giá cao một core library nhỏ gọn.

Cũng đừng quên một lựa chọn mạnh mẽ khác là React Hook Form, thường được biết đến với hiệu suất vượt trội và API Hook cực kỳ đơn giản, tập trung vào việc quản lý validation và submit hiệu quả mà không re-render form quá nhiều lần. Trong nhiều dự án mới, React Hook Form đang trở thành lựa chọn mặc định.

Lời Kết

Formik và Final Form đều là những thư viện tuyệt vời giúp đơn giản hóa việc xử lý form trong React. Formik mang đến một cách tiếp cận dựa trên quản lý state tập trung với API Hook và tích hợp validation schema dễ dàng, trong khi Final Form cung cấp sự linh hoạt cao hơn, tối ưu hóa hiệu suất re-render theo mặc định thông qua mô hình state-agnostic và Subscription.

Việc lựa chọn thư viện nào hoàn toàn phụ thuộc vào nhu cầu cụ thể của dự án, sở thích về API, và kinh nghiệm của team. Đừng ngần ngại thử nghiệm cả hai (hoặc cả React Hook Form) với một form đơn giản để cảm nhận sự khác biệt trước khi đưa ra quyết định cuối cùng.

Hy vọng bài viết này đã cung cấp cho bạn cái nhìn sâu sắc về Formik và Final Form, giúp bạn tự tin hơn khi đối mặt với những thách thức của việc xây dựng form trong ứng dụng React của mình. Hãy tiếp tục theo dõi series “React Roadmap” để khám phá thêm nhiều kiến thức hữu ích khác trên hành trình trở thành một React developer chuyên nghiệp!

Chỉ mục