---
# System prepended metadata

title: HKTVMall Revamp Techniques syncing

---

# React Hooks Form + yup


## :triangular_flag_on_post: 介紹

將表單中所有欄位的值交給 React Hooks Form 統一管理，在真正送出之前的驗證交給 yup 管理。
用 React Hooks Form 就不用自己把欄位寫成 state 個別管理，並且也處理了欄位錯誤的情形，並且提供了許多表單常見的 API ，而 yup 是個驗證條件的 schema 建構工具，只要將對應的欄位寫上驗證條件再丟給 React Hooks Form 去 mapping 就可以進行驗證，如此一來 code 裡面就不會在每個欄位都寫各自的驗證，全部統一在 yup 並且寫法也很簡潔，這樣整個表單流程頁就可以拆解成 欄位的值 + 驗證邏輯 + UI 三個部份

* 管理表單資料
* 送出表單資料
* 表單驗證
* 錯誤回饋
* 內建表單常用的方法
* 不同於 controlled component 他不會一直 rerender


## 解決方案

### 型別轉換

### 欄位監聽

```javascript=
  watch('name') // 監聽單一欄位
  watch(['name', 'age']) // 用陣列形式監聽多個欄位
  watch() // 監聽整張表
```

當某個欄位變動後需要執行與該 form 無關的操作(call API or UI chang)
```javascript=
// 監聽某欄位的變動並執行 callback
useEffect(() => {
  const subscription = watch((value, { name, type }) =>
  console.log(value, name, type)
 )
    return () => subscription.unsubscribe()
}, [watch])
```

antd 好像只能監聽單一欄位?!
```javascript=
  const skuIdVal = useWatch('sku_id', form);
  const productIdVal = useWatch('product_id', form);
  const originalPriceVal = useWatch('original_price', form);
  const colourFamiliesVal = useWatch('colour_families', form);
  const sizeSystemVal = useWatch('size_system', form);
  const option1Val = useWatch('option1', form);
  const option2Val = useWatch('option2', form);
  const option3Val = useWatch('option3', form);
```


### 欄位取值
getValues 並不會觸發重新渲染或是欄位的訂閱

```javascript=
const values = getValues() // 整張表
const singleValue = getValues("test") // 單一欄位
const multipleValues = getValues(["test", "test1"]) // 多欄
```

### 欄位賦值
setValue 並不會主動改變欄位的狀態(以下三種)，可以透過第三個參數來設定

```javascript=
setValue('name', value, {
  shouldValidate: true,
  shouldDirty: true,
  shouldTouch: true
})
```

### Touched and Dirty States

Touched : 代表使用者是否有與該欄位交互過(onBlur)，可以視為有沒有被摸過
Dirty : 代表使用者是否有被編輯過(onChange)

加上 valid state 是這個 library 基本且重要的概念，用來追蹤使用者與欄位的交互關係，進而建構出更好的 UX


### Disabling Fields
:::info
:bulb: disabled 請寫在 register option 裡面
:::
一般做法會加在 input element 上面，但是這只是阻擋使用者輸入，但在 react hook form 裡面要加在 register 的 option 中，這樣的 disabled 會除了無法輸入外也讓驗證失效以及欄位的值保持 `undefined`，可視為真正意義上的停用此欄位

可以搭配 `watch()` 來做 conditioal disabled，例如我在還沒填寫 A 欄位時候 B 欄位要 disabled 不可以填寫

### Handle Submission Error

handleSubmit 第一個參數處理整包合法的表單資料，第二個為錯誤時候的錯誤欄位

```javascript=
const onSubmit: SubmitHandler<Inputs> = (data) => console.log(data);
const onError: SubmitErrorHandler<Inputs> = (err) => console.log({ err });


<form onSubmit={handleSubmit(onSubmit, onError)}>
//... skip
```

### Reset Form
`reset` 將表單重設回預設，除了送出後或是某情狀清空表單外，應該也可以用來在填入 API 預設資料上，在參數中帶入 value 整個物件而不用單一欄位一個個去 `setValue`


### Disable Form Submission

在表單尚未填寫以及尚有欄位驗證未過時候將 submit 按鈕給 disabled，一種 for UX 的做法，`isDirty` `isValid` 用表單被填過以及驗證全過這兩個值達成

```javascript=
  const {
    formState: { isDirty, isValid }
  } = useForm<Inputs>();

  <button disabled={!isDirty || !isValid}>送出</button>
```

### Form Submission State
這個比較少使用的情境，也是 formState 物件裡面的值，基本上就是顧名思義大概知道一下，只有 `isSubmitting` 可以用在防止連點送出按鈕的情境上

`isSubmitSuccessful` 可以搭配 `reset` 來達成當表單確認送出成功時才執行
```javascript=
  const {
    formState: { isSubmitting, isSubmitted, isSubmitSuccessful, submitCount   }
  } = useForm<Inputs>();

  <button disabled={isSubmitting}>送出</button>
```

### Async validation (欄位驗證需透過 API)

例如 email 驗證需要透過 API 來確認是否已被註冊過時使用

```javascript=

```

### Validation Modes

`onChange | onBlur | onSubmit | onTouched | all = 'onSubmit' `

第一次驗證是在送出表單時觸發，可以依照需求來調整驗證的時機，送出前後的驗證模式可以分開個別為 `mode` and `reValidateMode`

為了提升 UX 可以在驗證後的模式改為 onChange，假設今天我是使用者，我在第一次填表格的時候不會被驗證而是讓我順暢的填寫完所有資訊，直到我按下送出按鈕跳出錯誤提示後，此時的就要讓使用者在輸入的同時可以立即得知已符合驗證條件

### Manually Trigger Validations

在非表單送出而已某些時機點要去進行驗證

```javascript!
 const { trigger } = useForm<FormInputs>()
 
 <button type="button" onClick={() => trigger()}/>// 驗證全部 
 <button type="button" onClick={() => trigger("lastName")/> // 單一驗證
```



## 其他

開發輔助工具
`npm install -D @hookform/devtools`

```javascript=
import { DevTool } from '@hookform/devtools';
```
放在 form 旁邊就可以使用了

在 Next13 要動態載入
```javascript=
import dynamic from 'next/dynamic';

const DevT: React.ElementType = dynamic(
  () => import('@hookform/devtools').then((module) => module.DevTool),
  { ssr: false }
);
```


## yup

定義一個驗證的物件 (schema)，表單中有多少欄位要驗證就寫多少，可使用 `.isValid` 在開發階段測試

```javascript
import * as yup from "yup";

export const profileSchema = yup.object({
  "first-name": yup.string().trim().required("請輸入姓氏"),
  "last-name": yup.string().trim().required("請輸入名稱"),
  "e-mail": yup
    .string()
    .email("請輸入一個有效電郵地址")
    .required("請輸入電郵地址"),
});

// 可以這樣自己測試
console.log(
  await profileSchema.isValid({
    "first-name": "Wang",
    "last-name": "Allen",
    "e-mail": "allen@gmail.com",
  })
);
```

### 連動驗證

#### [when](https://github.com/jquense/yup#schemawhenkeys-string--string-builder-object--values-any-schema--schema-schema)

當有欄位為依據其他欄位而有不同驗證條件時候使用，例如某個下拉選單選擇某個數量後，下面一個輸入欄位的數字就不可以小於剛剛所選的數量

#### [ref](https://github.com/jquense/yup#refpath-string-options--contextprefix-string--ref) 、 [oneOf](https://github.com/jquense/yup#schemaoneofarrayofvalues-arrayany-message-string--function-schema-alias-equals)

當需要取某個欄位的值可以使用 `ref(欄位名稱)`，

`oneOf([])` 裡面的陣列用來定義特定合法的值

```javascript=
export const registerSchema = yup.object({
  password: yup.string().trim().required("請輸入密碼"),
  confirmPassword: yup
    .string()
    .trim()
    .required("請輸入確認密碼")
    .oneOf([yup.ref("password")], "確認密碼必須與密碼相同"),
});
```

### 方法列舉

錯誤訊息都寫在 () 內

| Type |   顧名思義表   |
| ------ | ------- |
| Common | `.required()` |
| String | `.min()` `.max()` |
| Number | `.min()` `.max()` `lessThan()` `moreThan()` `positive()` `.negative()` `.integer()` |
| Array |    |

* Common
    * `.default()` 會有值但並不會直接顯示在 UI
* String
    * `.ensure()` 將 undefined、null 轉成 ''
    * `.trim()` 挺方便，不需要再寫在頁面上
    * `.lowercase()` `.uppercase()` 例如在填寫身分證時候就替使用者轉大寫
    * `.matches()` 自行比對正規
    * `.email()` `.url()` 已經做好了可直接用
* Number
    * `.truncate()` 截斷小數
    * `.round(type: 'floor' | 'ceil' | 'trunc' | 'round' = 'round')`
