【Laravel Inertia】useForm を使った値初期化の最適解

Laravel Inertia では、フォーム使用を簡単にするための `useForm` という関数が用意されている
これの使い方をまとめた記事

https://inertiajs.com/forms

----

## いつもの環境構築

```bash
$ curl -s https://laravel.build/example-app?with=pgsql | bash
$ cd example-app
$ ./vendor/bin/sail up -d

$ ./vendor/bin/sail composer require laravel/breeze --dev
$ ./vendor/bin/sail artisan breeze:install
$ ./vendor/bin/sail artisan migrate

$ ./vendor/bin/sail npm run dev
```

適当にフォームページを作る  
POST 先は `dd()` で止めておく  

```php:routes/web.php
use Inertia\Inertia;

Route::get('/form', function () {
    return Inertia::render('Form');
});

Route::post('/form', function () {
    dd(request()->input());
});
```

```vue:resources/js/Pages/Form.vue
<template>
  <form @submit.prevent>
    <input v-model="form.name">  

    <button @click="onSubmit">保存</button>
  </form>
</template>

<script setup lang="ts">
import { useForm } from '@inertiajs/vue3'

const form = useForm<{
  name?: string
}>({
  name: ''
})

const onSubmit = () => {
  form.post('/form')
}
</script>
```

ででん！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3833190/bb77bd15-7bf8-edce-090b-89f7a2d6ffd8.png)




## useForm の挙動

### 通常使用

```ts
const form = useForm<{
  name?: string
}>({
  name: ''
})
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3833190/deaf15b8-30cb-cf27-2812-63794657b055.png)


### 引数を空にしてみる

```ts
const form = useForm<{
  name?: string
}>({})
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3833190/e2f1783b-cd3c-7c3c-c124-b67d2a816be0.png)

おやぁ...キーすら送られない  


### 初期値を null にしてみる

```ts
const form = useForm<{
  name?: string | null
}>({
  name: null
})
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3833190/b2c11fc5-cae7-2316-6c4b-298515eccdea.png)

これは問題なく送信できている


### 初期値を undefined にしてみる

```ts
const form = useForm<{
  name?: string
}>({
  name: undefined
})
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3833190/df9b7d32-fac4-04b9-68ea-2cf7032527b6.png)

ふぅむ、こちらも送られない  

ただこの状態で input に文字を記入、削除を行うと、null となる

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3833190/05dfee46-331f-2498-f59f-255ac0467b14.png)

つまり、inputが空の状態だと null の扱いとなる  





## useForm の挙動の謎

弊社では useForm をダイアログを使ってよく使いまわしているのだが
useForm を使いまわそうと思ったら、初期値に無理やりキーと値を指定しないといけない  

初期値で仮の値を入れて、使う際に再度値をセットする...  
めんどくさい...  

なんで型情報を読み取ってくれないんだろうか :thinking:  
ソースコードを読みに行ってみた  

https://github.com/inertiajs/inertia/blob/master/packages/vue3/src/useForm.ts  

実際に `post` など、値の送信を行う際は `submit()` が実行される
その時に使われる値は `data()` メソッドで取り出した値だ

```ts
    submit(method, url, options: VisitOptions = {}) {
      const data = transform(this.data())
```

で、この `data()` は何をやっているかというと

```ts
    data() {
      return (Object.keys(defaults) as Array<keyof TForm>).reduce((carry, key) => {
        carry[key] = this[key]
        return carry
      }, {} as Partial<TForm>) as TForm
    },
```

わーお  
この `useForm`、既存のメソッドなどとフォームの値、分けないで保存してるのね
そのせいか `defaults` というオブジェクトのキーを基準としているみたい

`defaults` というのはこの useForm が作成されたタイミングで data をクローンしたものみたい  

```ts
let defaults = typeof data === 'object' ? cloneDeep(data) : cloneDeep(data())
```

この時の `data` は、`useForm(ここ)` を示す  
つまり、途中でなんか弄っても変わることはないんですね  

このオブジェクトのキーを基準としているので、初期値が `{}` だと何も送信されない結果となる

:::note info
TS は型情報が実行時に消されるため、このような実装になったのではないか  
型情報を for 文で回したりできれば、生成関数が作れるが

いっそ、キーだけ指定できる関数があれば解決？？
:::

ちなみに undefined が送信されないのは、確かajaxか何かの仕様だったはず（ソース不明）





## 対策

値の定義回数はできる限り減らしたい！

### defaults() を使う

先ほど `defaults` は useForm を作ったときにしか使えないと分かった
この値を変更する関数が用意されている

```ts
defaults(fieldOrFields?: keyof TForm | Partial<TForm>, maybeValue?: FormDataConvertible)
```

その後 `reset()` メソッドを使用すると、`useForm()` に値をセットしなおすことができる

```ts
reset(...fields)
```

それぞれ key に対して値を変更することもできるし、まとめて全フィールドを変更することもできる
これを使って  

```ts
const form = useForm<{
  name?: string
}>({})

const onSubmit = () => {
  form.post('/form')
}

onMounted(() => {
  form.defaults({
    name: 'なまえ',
  }).reset()
})
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3833190/aa79cf71-327e-2f3b-6669-00e3752a89a2.png)

この使い方が賢そう  

`onMounted()` のとこを、ダイアログが開いたときの `watch` 合わせればいい感じでは