React Form: Redux Form vs React Final Form vs Formik and Yup

React Form: Redux Form vs React Final Form vs Formik & Yup

本文會先從為何要做表單狀態管理說起,接著看目前市面上有哪些好的表單函式庫和條列挑選原則,並探討三個表單函式庫 Redux Form、React Final Form、Formik & Yup,最後做比較和總結。

為何需要做表單狀態管理?

為什麼需要做表單狀態管理呢?這就要從 controlled component 和 uncontrolled component 開始談起。

在一般 HTML 的世界裡面,表單的狀態是由元件本身來做儲存和更新的,稱之為「uncontrolled component」(下圖右);而在 React 的世界裡,表單的狀態和值的更新是由開發者處理,如下圖左所示,表單欄位的值可從 props 或 state 取得,在這裡是 state,並且當使用者打字等行為時觸發事件來做值的更新,在這裡是觸發 onChange 事件,這樣的元件稱為「controlled component」。這樣值的儲存與更新的管理方式,就是表單簡易的狀態管理的例子,表單還有其他狀態要管理,例如:表單是否合法(valid)、各欄位的錯誤訊息(validation、error)、欄位值是否被更改過(dirty)、初始值的設定(initial value)、各種 callback 的設定等。

Controlled vs uncontrolled component

也因為由開發者來管理表單的狀態,所以我們可能需要自己刻一套管理工具,或是使用市面上大師們已經實作好的函式庫就好了,不管是自已刻還是用別人刻好的,在專案的開發上,都可以達到兩個效果…

當紅的表單函式庫

先不要談自己刻這件事,來看看大師們幫我們刻好的表單函式庫有哪些…

Popular form libraries in React

由上圖可知,可選擇的表單函式庫真的很多,大致上可分類為功能完整、輕量或內建驗證工具等共三種,另外還有一些是進入維護狀態、不再開發新功能的,就不納入評選。

接下來我選擇 Redux Form、React Final Form 和 Formik & Yup 來做討論。

如何選擇好的表單函示庫?

先綜觀來看怎麼選擇好的表單函示庫。一般來說,這些函式庫主要都是做狀態管理,而它們都做得很好,只是有些差異,因此歸納了一些挑選原則…

Redux Form

Redux Form

Redux Form 利用 Redux 來儲存整個 app 的表單狀態,再根據當前所需提取特定表單資料。也就是說,先在 Redux store 建立表單的 reducer,經由 HOC 的方式連接表單和 store,再由 props 帶入資料。

Workflow

Redux Form Workflow

圖片來源:Redux Form - Getting Started

Redux Form 的工作流程是這樣的…

當使用者對表單元件輸入資料時會發出 action 去更新 store,當狀態被更新,就會重新渲染元件,使用者就能看到剛才輸入的資料。

Demo

範例 1

這是一個簡易的表單範例,並且使用 redux-form-validators 作為驗證的工具。

Redux Form 範例

在這個範例中有兩個欄位 Name 和 Email,初始值就不是合法的了,所以若在此時按下按鈕 Submit 提交表單,會看到 Name 下方紅色的錯誤訊息「Length must exceed 3 characters.」

Redux Form 範例

在修正欄位值後,就可以順利提交了。

Redux Form 範例

來看原始碼,如果想得到特定表單的資訊,必須從 reducer 中使用 selector 提取出來,然後再用 props 帶進表單,例如:formData、formValues 和 formErrors。formData 會取得目前表單所有欄位的狀態,例如這個欄位是否被 touched、dirty 或目前 focus 在哪個欄位等;formValues 取得目前表單各欄位的值;formErrors 取得目前表單各欄位的錯誤訊息。

Form = connect((state) => ({
  formMeta: getFormMeta('register')(state), // get form data by using selectors
  formValues: getFormValues('register')(state),
  formErrors: getFormSyncErrors('register')(state),
}))(Form);

在 selector 這裡要輸入唯一的表單名稱「register」,我常常不是打錯字就是輸入重複的名稱,感到困擾和麻煩 XD

另外,在這裡搭配 redux-form-validators 作為驗證工具,它提供一些簡易的 field-level validation 驗證規則,並且可自訂錯誤訊息,也可經由 addValidator 加入驗證規則。

簡易使用方式如下,將 name 這個欄位加上驗證規則必填(required)和字數限制(length,必須超過 4 個字),並自訂報錯訊息。

Demo原始碼

<Field
  name='name'
  validate={[
    required({ message: 'Required.' }),
    length({ min: 4, message: 'Length must exceed 3 characters.' }),
  ]}
/>

範例 2

這是一個混合同步和非同步驗證的範例,同步驗證像是名稱是否必填、字數限制至少 4 個字,email 是否合法;非同步驗證像是詢問使用者的名稱是否已被使用,輸入「paul」表示已被別人使用了,這是模擬從伺服器端回傳驗證結果,再顯示錯誤訊息的範例。

Redux Form 範例

Demo原始碼,其他範例還有建立動態欄位

優點

缺點

關於以上的缺點,不管是表單狀態的儲存、打包大小、文件與範例、擴充性等,接下來我們會看到兩個做得更好的函式庫-React Final Form 和 Formik。

備註:減少不必要的渲染是指在 React 做 virtaul dom 的比對而決定是否要渲染前,可利用元件實作的機制而省下這複雜計算過程,因此能減少延遲效果。

Final Form

Final Form

在看 React Final Form 之前,先來看它的狀態管理引擎 Final Form。由於 Final Form 是表單狀態管理的引擎,因此與框架無關,並且可以獨立使用或實作 React 或 Vue 的 wrapper 包裝後來使用它。

Final Form 的特點如下

Demo

範例 1

這是一個簡單的範例,示範如何使用 Final Form。

如下圖所示,同樣也是填寫 Name 和 Email 的簡易表單,由於 Name 必須超過 3 個字,因此提交按鈕是 disabled 狀態,無法送出。

Final Form 範例

blur 欄位 Name 後可看到錯誤訊息。

Final Form 範例

修正欄位後,提交按鈕變成 enabled 狀態,即可送出。

Final Form 範例

從實際程式碼來看 Final Form 的使用方式。

利用 createForm 建立表單,並指定三個參數 initialValues、onSubmit、validate,分別是表單的初始值、提交時呼叫的 callback 和驗證規則,其中 onSubmit 必填。

import { createForm } from 'final-form';

const form = createForm({ initialValues, onSubmit, validate });

訂閱表單要監聽的屬性,例如:表單是否合法(valid)、目前在哪個欄位(active)、目前表單所有欄位的值(values)、哪些欄位已被修改過(dirty)。

// Subscribe to form state updates
const unsubscribe = form.subscribe(
  formState => {
    // Update UI
  },
  { // FormSubscription: the list of values you want to be updated about
    active: true,
    dirty: true,
    valid: true,
    values: true
  }
})

訂閱欄位要監聽的屬性,例如:錯誤訊息(error)、是否被觸碰(touched)、目前的值(value)等。

// Subscribe to field state updates
const unregisterField = form.registerField(
  'name',
  (fieldState) => {
    // Update field UI
    const { blur, change, focus, ...rest } = fieldState;
    // In addition to the values you subscribe to, field state also includes functions that your inputs need to update their state.
  },
  {
    // FieldSubscription: the list of values you want to be updated about
    error: true,
    touched: true,
    value: true,
  },
);

Demo原始碼

範例 2:React wrapper for Final Form

簡單實作 React wrapper 來使用 Final Form。同樣也是填寫 Name 和 Email 的簡易表單,由於 Name 必須超過 3 個字,因此提交按鈕是 disabled 狀態,無法送出。

Final Form 範例

程式碼範例如下,這裡用一個 Form wapper 把 Final Form 包起來,開發者只要依舊指定 initialValues、onSubmit、validate,並且設定欄位要顯示的名稱和屬性 name。表單指定要監聽四個屬性 valid、pristine、submitting、values,欄位則是沒有指定就是監聽全部屬性。

備註:由於部落格會把花括號吃掉,因此在左右加一個點,例如「.{.{ }.}.」。

<Form // (1) create form
  initialValues=.{.{
    name: 'Ann',
    email: 'sample@test.com',
  }.}.
  onSubmit={(values) => {
    alert(JSON.stringify(values, 0, 2));
  }}
  validate={validate}
  // (2) subscribe form state
  subscription={['valid', 'pristine', 'submitting', 'values']}
>
  {/* (3) subscribe all field state */}
  <Form.Field label='Name' name='name' />
  <Form.Field label='Email' name='email' />
</Form>

Demo原始碼

React Final Form

Reacg Final Form

Workflow

Workflow

說明

Demo

範例 1:Decorators

React Final Form 的擴充性高,可利用 decorator 的方式輕易的新增或移除功能以符合未來的需求,並且也能保持 React Final Form 的體積很小。

例如,我們希望在提交表單後,能 foucs 在第一個有錯的欄位上,那就可以裝上 final-form-focus 這個 decorator 協助達成。

如下圖所示,表單的兩個欄位 Name 與 Password 皆為必填但未填,因此按下提交按鈕後,都顯示錯誤訊息,並因為裝了 final-form-focus 而能將游標停在第一個欄位,也就是欄位 Name 當中。

React Final Form: Decorators

Demo原始碼

範例 2:利用訂閱機制改善效能問題

一開始這個表單的欄位全部都沒有通過驗證規則,例如:name 為必填但是目前沒有值,email 不合規則,此時表單訂閱的狀態中 valid 值為 false。

React Final Form: 利用訂閱機制改善效能問題

name 和 email 彼此不會影響渲染,只有自己狀態改變,例如因打字輸入而改變 value 時才會重新渲染。

React Final Form: 利用訂閱機制改善效能問題

由於表單有訂閱狀態 valid,因此只有當全部欄位都通過驗證,也就是 valid 由 false 變成 true 時,表單才會重新渲染,可以看到渲染次數由 2 變成 3。

React Final Form: 利用訂閱機制改善效能問題

優點

缺點

Formik & Yup

Formik

Workflow

Formik 的工作流程是這樣的…

Workflow

說明

Yup 好在哪裡?

不需再看到 if/else 判斷句

傳統的寫法是用 if/else 根據條件判斷要用哪些規則,雜亂且難以維護。如下範例,表單欄位中有 name 這個欄位,若 name 沒填則顯示錯誤訊息「Required.」(必填),若 name 的字數小於 4 個字,則報錯「Length must exceed 3 characters.」(字數需要超過 3 個字)。

if (!values.name) {
  errors.name = 'Required.';
} else if (values.name.length < 4) {
  errors.name = 'Length must exceed 3 characters.';
}

利用 Yup 改寫後,簡單清楚易懂。

const schema = Yup.object().shape({
  name: yup
    .string()
    .required('Required.')
    .min(4, 'Length must exceed 3 characters.'),
});

Cross-validation

下圖是一個簡單的電子報訂閱表單的範例,驗證規則是如果沒有勾選要訂閱電子報(subscribe)的話,填完名稱(name)就可以送出;若要訂閱電子報,勾選「訂閱電子報」之後,信箱(email)欄位成為必填,因此就必須填寫。

Formik: Cross-validation

利用 Yup 可以很輕易地做到根據條件動態決定驗證規則,如下程式碼所示,email 的規則是根據 subscribe 的值而決定的,when 後接要觀察的欄位名稱,在此觀察 subscribe 這個 checkbox 是否被勾選,若有勾選(亦即 is 為 true),則做 then 後所接的事情,否則做 otherwise 後所接的事情,因為這裡沒有「否則」要做什麼,所以程式碼中就沒有示範了。

email: yup.string().when('subscribe', {
  is: true,
  then: fieldSchema => fieldSchema
    .required('Required.')
    .isEmail('Invalid email address.'),
}),
subscribe: yup.boolean(),

Demo原始碼

可混合同步和非同步的驗證

可混合同步和非同步的驗證,而且可以用這樣的方式撰寫 .sync().async().sync().async()…簡潔易懂。

validationSchema: yup.object().shape({
  name: yup
    .string()
    .required('Required.')
    .isNameAvailable('Name is taken!') // 非同步
    .min(4, 'Length must exceed 3 characters.')
  })
})

Demo原始碼

有用的小工具

Yup 提供一些有用的工具,像是去除前後空白、將字串轉為全部大寫或小寫等。

如下範例所示,當使用者輸入字串時,可能前後都有空白,這時候 Yup 可先將字串去除前後空白後再做驗證,而不會造成空白也是一個字元的狀況。

例如,在以下程式碼的狀況下,輸入五個空白或字串「Alice」,都是可以通過驗證的。

name: yup.string().required('Required.');

若加上 trim(),則五個空白會被去除,而顯示錯誤訊息;字串「Alice」依然可以通過驗證。

name: yup
  .string()
  .required('Required.')
  .trim();

Demo原始碼

FastField

若只是使用一般的<Field>,則每次欄位更新時,其他欄位都會一同重新渲染;但若改成 <FastField> 則沒有相依關係的欄位就不會重新渲染。這是由於 <FastField> 內部實作 shouldComponentUpdate() 來決定是否要重新渲染的緣故。

下圖是 Formik 的 <FastField> 的流程圖,只有需要被更新的欄位(direct update)才會重新渲染,否則就 block 住。

Workflow

來看一個範例,如下圖所示,這是一個簡易的表單,包含兩個欄位 Name 和 Password,右邊灰色圓圈內的數字表示元件的渲染數,由上而下依序是 Name 欄位(FastField)、Password 欄位(FastField),一開始數字都是 1。

Formik FastField Example

當對 Name 欄位輸入資料「Summer」時,由於只有 Name 欄位是會被直接更新的,因此旁邊只有 Name 欄位旁邊的灰色小圈圈數字會增加。

Formik FastField Example

Demo原始碼

優點

缺點

雖然功能包山包海,功能齊全,但相較 Final Form 可用 decorator 來擴充功能,彈性較小。

Summary

如何選擇好的表單函示庫?

再次回顧本文一開始提到的,要怎麼選擇好的表單函式庫呢?我們可以考慮以下幾點…

以下一一說明。

狀態管理

程式碼撰寫

Redux Form、React Final Form 和 Formik 皆提供足夠的 API 以實作細緻的表單,因此節省開發人員不少時間。

驗證

Extensibility

Examples and documents

Bundle size

Bundle size

Github stars and NPM downloads

Github stars and NPM downloads

總結

在經過以上探索與討論後,做個總結…

在目前我所經手的專案上,由於 (1) 對表單功能有強烈需求,必須顧慮開發上的便捷,並且 (2) 專案很老了,常常需要重構,要能有效整理程式碼,因此 Formik & Yup 對我而言就是很好的選擇;而也有些專案對體積大小斤斤計較、並且不需要這麼多功能,或必須維持高彈性、因應需求端的快速變化,那就很適合使用 React Final Form。

話說這麼多,就是希望大家在茫茫大海中,都能找到適合自己的表單函式庫摟!👍 👍 👍

References

後記

本文投稿至 TechBridge 於 2019/5/19 通過 🎉 文章在此,歡迎指教 🙏🏼


react.js form react.js redux virtual dom sharing