Next.js 15で追加されたFormコンポーネントで遷移前に処理を行う

## `next/form`
`Form`コンポーネントがNext.jsのバージョン15で追加されました。

https://nextjs.org/blog/next-15#form-component

`Form`は`form`を拡張したコンポーネントで、画面遷移する検索フォームやサーバーアクションによるミューテーションで利用されます。

```tsx
import Form from 'next/form'
import { createPost } from '@/posts/actions'

------------------------------------------------
// 検索フォーム
function Page() {
  return (
    <Form action="/search">
      <input name="query" />
      <button type="submit">Submit</button>
    </Form>
  )
}
------------------------------------------------
// サーバーアクション
function Page() {
  return (
    <Form action={createPost}>
      <input name="title" />
      <button type="submit">Create Post</button>
    </Form>
  )
}
```
`action`には文字列と関数(サーバーアクション)を渡せます。

文字列が渡されたときは送信のタイミングでナビゲーションが行われます。遷移先は`action`に渡した文字列をパスに、フォームデータを元にした検索パラメータを組み合わせたURLになります。上記の例では`/search?query=xxx`に遷移します(`xxx`はinputの入力)。

ナビゲーションは`Link`コンポーネントと同じような振る舞いをします。JavaScriptが読み込まれていないときはハードナビゲーションが、読み込まれている場合はソフトナビゲーションが行われます。
さらに、フォームが表示されたタイミングで遷移先のレイアウト(`layout.tsx`)と読み込み状態(`loading.tsx`)を事前に取得します。

:::note
<details><summary>これらの機能を持つ`Form`コンポーネントを実装した例</summary>

```tsx:Next15のBlogより引用
// Note: This is abbreviated for demonstration purposes.
// Not recommended for use in production code.
 
'use client'
 
import { useEffect } from 'react'
import { useRouter } from 'next/navigation'
 
export default function Form(props) {
  const action = props.action
  const router = useRouter()
 
  useEffect(() => {
    // if form target is a URL, prefetch it
    if (typeof action === 'string') {
      router.prefetch(action)
    }
  }, [action, router])
 
  function onSubmit(event) {
    event.preventDefault()
 
    // grab all of the form fields and trigger a `router.push` with the data URL encoded
    const formData = new FormData(event.currentTarget)
    const data = new URLSearchParams()
 
    for (const [name, value] of formData) {
      data.append(name, value as string)
    }
 
    router.push(`${action}?${data.toString()}`)
  }
 
  if (typeof action === 'string') {
    return <form onSubmit={onSubmit} {...props} />
  }
 
  return <form {...props} />
}
```
[実際にnextから提供されている`Form`コンポーネントの実装はこちら](https://github.com/vercel/next.js/blob/f3533cb2236db06a94a0b8b2134ca626b002286b/packages/next/src/client/form.tsx)
</details>
:::

## 遷移前に処理を行う
`Form`を用いた遷移の前に、フォームデータのバリデーションのような何らかの処理を挟みたい場合は、`Form`に`onSubmit`を渡します。
```tsx
function Page() {
  const handleSubmit = (e) => {
    validateFormData(e.currentTarget);
  };

  return (
    <Form action="/search" onSubmit={handleSubmit}>
      <input name="query" />
      <button type="submit">Submit</button>
    </Form>
  )
}
```
上記の例では遷移の前に`validateFormData`が実行されます。

これだけでは`validateFormData`の結果によらず、実行後に規定の遷移を誘発してしまいます。
`onSubmit`の処理によって、遷移を停止したい場合は、`e.preventDefault()`を呼び出します([参考](https://github.com/vercel/next.js/blob/f3533cb2236db06a94a0b8b2134ca626b002286b/packages/next/src/client/form.tsx#L213))

```tsx
function Page() {
  const handleSubmit = (e) => {
    const result = validateFormData(e.currentTarget);
    if (!result.success) {
      e.preventDefault();
    }
  };

  return (
    <Form action="/search" onSubmit={handleSubmit}>
      <input name="query" />
      <button type="submit">Submit</button>
    </Form>
  )
}
```
この例では`validateFormData`が失敗した場合は`e.preventDefault()`呼ばれるので、遷移が行われません。

特定の値を返したり、早期returnすることで遷移されない期待も抱いてしまうのは否めないので、`e.preventDefault()`を忘れないようにしましょう。