iT邦幫忙

2023 iThome 鐵人賽

DAY 15
2
SideProject30

營養師不開菜單要用 Next.js 13 寫全端系列 第 15

營養師不開菜單的第十五天 - 為什麼從 Formik 跳槽到 React-Hook-Form

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20230929/20152073AQZS1hTO5R.png

React-Hook-Form 是一個輕量的 React 表單驗證和管理套件,核心是運用 React Hooks API 來進行管理狀態和表單操作,讓 React 使用者可以更容易上手。除了此之外,不僅提供強大的驗證工具,如自訂驗證、異步驗證及條件驗證,也有內建錯誤處理及支援多語系功能。

為什麼要用 React-Hook-Form?


在寫這篇文章之前,其實我在專案中使用的是 Formik,因此我陷入了非常煎熬的天人交戰,雖然用過幾次 React-Form-Hook,但我的習慣及套件使用經歷全部都傾向了 Formik,而且專案已經開發到一半,這時候中途替換套件,會不會又是成本之一?但撼動我心中最後的一根稻草就是因為 Formik 在每次 onChange 都會觸發表單的重新渲染,雖然表單大部分都是在後台中使用,但考量到若是之後有更多的客製化功能需要更大量的表單欄位來呈現,這個問題可能成為一個棘手的效能風險,所以趁早改成低量渲染的套件是我最後的決定。

至於為什麼要換成 React-Hook-Form 呢,以下來一一條列:

  1. 提升效能與減少渲染:React Hook Form 的核心是使用 Uncontrolled Component,意思是依賴 DOM 本身的狀態來管理資料。因此,只有在驗證錯誤或提交表單時才會觸發重新渲染,來有效提高效能。

  2. 給予高度自由度:React Hook Form 並不強迫開發者使用特定的 UI 元件。這提供了很大的彈性,讓開發者更容易地將自訂元件與該套件功能結合。

  3. 直覺的 Hooks 操作:React-Hook-Form 提供了一系列的 hooks,如 useFormuseFieldArray,使開發者可以在組件中輕鬆地管理和操作表單狀態。

  4. 支援多樣驗證工具:開發者不僅可以根據需求自訂驗證方法,還可以與 Yup, Zod, Superstruct 以及 Joi 這些受歡迎的驗證工具輕鬆集成 (藉由提供 resolvers 來實現)。

  5. 簡單的套件轉換:基於上面 2 ~ 4 點,與我先前使用 Formik 的模式非常相似 ( useFormik 方法 ),所以在套件轉移上降低許多時間成本。

  6. 輕量且無額外依賴套件:依據 bundlephobia 提供的數據,MINIFIED + GZIPPED 之後的大小為 9.7 Kb

    https://ithelp.ithome.com.tw/upload/images/20230929/20152073eks8ysNQP9.png

  7. 市佔率及社群熱度:前端套件推陳出新,但這兩年 React-Hook-Form 已成為下載量的領頭羊,github 的星星數也有 3.6k,社群討論及 issue 處理還有套件更新都非常積極。

    https://npmtrends.com/formik-vs-react-final-form-vs-react-hook-form

    https://ithelp.ithome.com.tw/upload/images/20230929/201520738NnL3eJm6u.png

    https://ithelp.ithome.com.tw/upload/images/20230929/20152073WzgCpf74aG.png

useForm options 屬性


useForm 是操作整個表單管理的主要 hook,可以傳遞 options 設定並回傳各種操作 function,首先介紹 options 的屬性:

  1. defaultValues: 指定表單欄位的預設值。如果表單是修改模式需要預設帶原始數據,可以將變數設定在這個屬性中,如下方範例。

  2. values:當外部狀態或資料變更時,自動更新表單的值。

  3. shouldFocusError: 當驗證失敗時,這將自動將焦點設定到產生錯誤的第一個欄位。預設 true

  4. mode: 指定驗證模式,例如 onChange, onBlur, onTouched, onSubmitall。預設是 onSubmit

  5. reValidateMode: 指定重新驗證的模式。可選擇 onChange, onBluronSubmit ,預設是 onChange

  6. resetOptions:當 valuesdefaultValues 被更新時,內部會呼叫 reset API,預設是重置表單的值,但可以額外設定 keepDirtyValueskeepErrors ,例如:希望保留使用者所互動的/已更改的值,並且不移除任何錯誤標示。

    • 如果沒有設置時,調用 reset 後,isDirty 會變成 false,input value 也被替換成初始值

      https://imgur.com/RbHnpXL

    • 若將 keepDirtyValues 設為 true,按第一下 reset,isDirty 會變成 false,input value 依舊是剛剛 dirty 的值,直到按第二下 reset,才會將 input value 換成初始值。

      https://imgur.com/qB50yjr

  7. shouldUnregister:定義已經 unmount 的欄位是否應該從表單中取消註冊。預設為 false。若設為 true ,則當輸入元件從 DOM 中被移除,其值也會從 react-hook-form 的內部狀態中被移除。

  8. criteriaMode: 決定是否回傳所有的錯誤。firstError 為預設,僅回傳第一個錯誤;all 則是回傳所有錯誤。

    https://ithelp.ithome.com.tw/upload/images/20230929/20152073HN6RHcYfd2.png

  9. shouldFocusError: 提交表單後,如果有錯誤,將 focus 移到該錯誤上。預設是 true

  10. shouldUseNativeValidation:使否使用瀏覽器原生的驗證,預設為 false

    https://ithelp.ithome.com.tw/upload/images/20230929/2015207317t3hdjqhU.png

範例

const form = useForm<FieldValues>({
    defaultValues: {
      id: item?.id || '',
      title: item?.title || '',
      url: item?.url || '',
      type: item?.type || { id: 'default', label: '請選擇' },
      order: item ? index : lastItemOrder + 1
    },
		resetOptions: {
      keepDirtyValues: true
    },
		criteriaMode: "all",
    mode: "onChange"
  })

useForm 回傳功能


包括 register, formState, watch, handleSubmit, reset 等表單操作功能

register

(name: string, RegisterOptions?) => ({ onChange, onBlur, name, ref })

在 React-Hook-Form 中註冊 input 或 selector 元素,並附加驗證規則。所有的驗證規則都依循 HTML 標準,同時也支持自定義驗證方法。

name:在表單資料中識別該輸入的值 (該輸入元素的 id)

options :可設置輸入資料驗證方法。

<input
	{...register("firstName", {
	  required: true,
	  disabled: true,
	  // pattern: /[A-Za-z]{3}/
	  // validate: // 自訂義驗證邏輯
	  // ...其他屬性
	})}
	placeholder="First name"
/>

// 或是包含錯誤訊息的設定
<input
    {...register("firstName", {
      required: {
        value: true,
        message: "Required!"
      }
        //...其他屬性
    })}
    placeholder="password"
/>

watch

watch 用於監聽指定或全部的 field 的值。當 input 值發生變化時,watch 會返回最新的值。會使表單 re-render。

// form.tsx

const type = watch('type') // 導致 re-render

<Selector
 id="type"
 value={type}
 options={linkList}
 onChange={(value) => setCustomValue('type', value)}
 error={errors.type?.message as string}
/>

為了縮小 re-render 的範圍,React-Hook-Form 提供了 useWatch 的 hook 讓開發者在 ‘需要監聽’ 的 component 中再使用 watch 功能。

// Selector.tsx
import { useWatch } from 'react-hook-form'

const Selector: React.FC<LinkItemProps> = ({
  control, // useForm 回傳的 control
  onChange,
  defaultValue
}) => {
	const value = useWatch({
    control,
    name: id,
    defaultValue
  })
	
  return (
    <>
      <Listbox value={value} onChange={onChange}>
        ...
      </Listbox>
    <>
)}

handleSubmit

((data: Object, e?: Event) => Promise<void>, (errors: Object, e?: Event) => void) => Promise<void>

如果通過驗證,會接受表單數據。接受 onSubmitonError 兩個 handler,onSubmit 的參數(下方範例中的 values )會帶入更新後的所有表單數據;onError 的參數則帶入 errors 的物件。


const Form = () => {

	const onSubmit: SubmitHandler<FieldValues> = async (values) => {
    try {
			<------- API 請求操作 -------->
    } catch (error) {
      <------- 請求失敗操作 -------->
    }
  }

	return (
		<form className="contents" onSubmit={handleSubmit(onSubmit)}>
		...
		</form>
	)
}

formState

為一個物件,包含了整個表單狀態的資訊。可以追踪使用者與表單應用程式的互動情況。

包含 isDirty(Boolean), defaultValues(Object), errors(Object), isSubmitted(Boolean), isSubmitSuccessful(Boolean), isSubmitting(Boolean), isLoading(Boolean), isValid(Boolean), isValidating(Boolean)等狀態。

setValues

(name: string, value: unknown, config?: Object) => void

手動設置一個或多個 input field 的值

✅使用場景:當元件不是依賴 register 操作時

const Form = () => {

	const setCustomValue = (id: string, value: any) => {
    setValue(id, value, {
      shouldValidate: true,
      shouldDirty: true,
      shouldTouch: true
    })
  }

	return (
		<form className="contents" onSubmit={handleSubmit(onSubmit)}>
			<ButtonGroup
				{... props}
				onChange={(value) => setCustomValue('themeColor', value)}
            />
				...
		</form>
	)
}

getValues

(payload?: string | string[]) => Object

一個用於讀取表單值,與 watch 的差別在於 getValues 不會使表單 re-render,所以也不會持續監聽 input 的變化。若無帶入 payload 表示取得表單中所有值。

✅使用場景:當我有兩個按鈕(預覽及儲存)皆需要取得表單中所有數值,但只有在儲存時調用 onSubmit 可以從參數取得所有資料,而預覽按紐的 handler 則需要藉由 getValues 取得所有數值。

const handlePreview = () => {
    const adminValues = getValues()
    update({ admin: adminValues })
  }

reset

重置整個表單狀態、欄位 reference 和訂閱。官方建議在表單送出後如果希望可以重置表單,調用的時機為使用 useEffect 並依據 formState 的回傳狀態進行操控,而不是在 onSubmit 中調用。

	const {
    register,
    handleSubmit,
    reset,
    formState: { errors, defaultValues, isSubmitSuccessful },
  } = useForm<FieldValues>({
    defaultValues: {
      username: "",
      password: "",
      firstName: ""
    },
	)

  const onSubmit = (data: FieldValues) => {
    console.log(data)
		// 不是在這裡調用 reset()
  }

	useEffect(() => {
		if(isSubmitSuccessful){
          reset(defaultValues) // 直接重置回初始值
            // 或是重置為自訂義 value 
            // reset({ username: "groot", password: "", firstName: "I'm"}) 
            // 或是 reset()
		}
	}, [isSubmitSuccessful])

trigger

(name?: string | string[]) => Promise<boolean>

此功能為可以觸發整個表單或單一/多個 input field 的驗證機制,並且回傳一個 resolve 為布林值的 Promise。

	const {
    getValues,
    trigger
  } = useForm<FormValues>({
    defaultValues: {...},
    resolver: zodResolver(schema)
  })

	const handlePreview = async () => {
    const result = await trigger()
    if (!result) return

    const adminValues = getValues()
    update({ admin: adminValues })
  }

<Button
  label="預覽"
	<---- 其他屬性 ---->
  onClick={handlePreview}
/>

驗證設置


React-Hook-Form 除了在 register 中可以自訂義驗證方法,也提供第三方的驗證套件,但需要先安裝 resolver 並作為 useForm 的參數使用

安裝

npm install @hookform/resolvers zod

設置


import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import * as z from "zod"

const schema = z.object(
	...
)
type Schema = z.infer<typeof schema>

const Form () => {
	const { register, handleSubmit } =
    useForm <Schema>({
    defaultValues: {
      username: "",
      password: "",
      firstName: ""
    },
		resolver: zodResolver(schema),
	)

	return (
	...
	)
}

結語

雖然說 React-Hook-Form 主打非受控元件的操作,但當真的遇到 UI 元件時還是會需要監聽表單的數值,好里加在套件本身也是提供 control 的方法可以讓使用者在需要的地方調用監聽,雖然依舊會 re-render 但縮小到最低受影響範圍,也讓表單的效能更好控制,這部分是在進行套件轉換時感到最特別的地方。而在設置完驗證的操控方法後,我們還沒實作到驗證的核心方法,明天接續著以第三方驗證套件的驗證方法吧!

參考資料

https://bundlephobia.com/package/react-hook-form@7.46.1
https://pjchender.dev/react/note-react-hook-form/#register
https://codesandbox.io/s/react-hook-form-v6-errors-validatecriteriamode-all-p9xm6?file=/src/index.js:361-406

https://ithelp.ithome.com.tw/upload/images/20230929/20152073MrY2OVIfQK.png


上一篇
營養師不開菜單的第十四天 - 為什麼要用 React-Beautiful-Dnd 做拖曳效果
下一篇
營養師不開菜單的第十六天 - TypeScript 不夠?使用 Zod 做型別驗證
系列文
營養師不開菜單要用 Next.js 13 寫全端30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言