React Form Hook とGoogle Formの連携

## はじめに
Webサイトを作成する際にフォームが必要になること多いですよね。フロントエンドは比較的簡単に実装できますが、フォームのためだけにバックエンドを実装するのは面倒です。Google Formは便利ですが、そのままの見た目はちょっとデザイン的にアレですよね...
今回はReact Form Hook と　Google Formを連携させてお手軽にフォームを作成する方法をご紹介します。ついでにValidationのためにZodも使用します。 ValidationはReact Form Hook単体でもできますが、Zodを使用することで、JSXがすっきりとした見た目になります。 加えて、TypeScriptを使用する際の型を提供してくれます。

## 前提
Google Formを作成した経験がある
React Form Hookを使用した経験がある

*Zodを使いますが、前提知識は不要です*

## まずはGoogle Formを作成
今回は検証できれば良いので名前と年齢の2つにします
![google fform.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1390680/1935e2ba-c94c-5b84-0372-1ff61f45820a.png)

## Reactプロジェクトを作成
筆者は普段、viteでReactを作成しています。
```shell
npm create vite@latest
```
TypeScriptは使用しないので`React > JavaScriptのテンプレート`を選択してください
`App.css`と`index.css`の内容を全て削除します。
index.cssに下記cssを追加してください
```css
* {
  box-sizing: border-box;
}

p {
  padding: 0;
  margin: 0;
  margin-top: 10px;
}

button {
  margin-top: 10px;
}
```

必要なpackageをinstallします
```shell
npm install react-hook-form
```

Zodを使用するので下記2つもインストールしてください
```shell
npm install @hookform/resolvers 
npm install zod
```


## App.jsxにコードを追加する
今回使用する全てのコードです。
`FormURLのurl`と`FormFieldNames`の内容は、後程各自が作成したGoogle Formから取得します
```jsx
import "./App.css";
import { useForm } from "react-hook-form";

// Zod
import { zodResolver } from "@hookform/resolvers/zod";
import * as zod from "zod";

// Zodのschema定義 (validation)
const schema = zod.object({
  name: zod.string().min(1, { message: "必須項目です" }),
  age: zod.preprocess(
    (v) => Number(v),
    zod.number().min(2, { message: "年齢は2桁以上である必要があります" })
  ),
});

// Google FormのActon
const FormURL =
  "https://docs.google.com/forms/u/0/d/e/1FAIpQLSe6jUbmv6snQNZV-dm8zyqB-9-z7AIGZ2Y5wBAjTJCKW3hScw/formResponse";

// Google Formから取得したname属性の値
const FormFieldNames = {
  name: "entry.587594010",
  age: "entry.414111699",
};

function App() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm({
    // zodを使用する設定
    resolver: zodResolver(schema),
  });

  const onSubmit = async (data) => {
    try {
      await fetch(FormURL, {
        method: "POST",
        // urlEncodeされた値をbodyに入れる
        body: provideUrlEncodedFormData(data),
        // 通常ではcorsに弾かれる (返却値のstatusは常に0 後述...)
        mode: "no-cors",
        headers: {
          "Content-Type": "application/x-www-form-urlencoded",
        },
      });

      // 成功後の処理を記述する (画面遷移とか)
    } catch (e) {
      alert("送信に失敗しました。ネットワーク状況を確認してください");
    }
  };

  return (
    <>
      <form onSubmit={handleSubmit(onSubmit)}>
        <div className="fom-item">
          <label>
            <p>名前</p>
            <input type="text" {...register("name")} />
          </label>
          <p className="error-message">{errors.name?.message}</p>
        </div>
        <div className="form-item">
          <label>
            <p>年齢</p>
            <input type="text" {...register("age")} />
          </label>
          <p className="error-message">{errors.age?.message}</p>
        </div>
        <button type="submit">
          送信
        </button>
      </form>
    </>
  );
}

// request bodyに必要なurlEncodeされた値を提供
// originalのform情報を{entry.{number}: value}に変換する
function provideUrlEncodedFormData(originalFormData) {
  const result = {};
  Object.keys(originalFormData).map(
    (key) => (result[FormFieldNames[key]] = originalFormData[key])
  );
  return new URLSearchParams(result).toString();
}

export default App;

```

## Google Formから必要な情報を取得する
App.jsxのうち`FormURLのurl(action)`と`FormFieldNamesのentry`をGoogle Formから取得します。

Google Formの右上の目玉マーク(プレビュー)を押します
画面サイズが小さいと隠れてしまうので気をつけてください
![スクリーンショット 2023-11-26 16.26.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1390680/5d65d414-89f1-4f99-bb5d-b7a8584340e2.png)

表示されたプレビュー画面で検証ツールを開き`<form`で検索するとactionを見つけることができます
![スクリーンショット 2023-11-26 16.39.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1390680/d4737ae8-6b1b-4f74-6eb2-080ab47e30ab.png)


inputのentryはプレビューに仮データを入力することで簡単に見つけることが可能です
![スクリーンショット 2023-11-26 16.38.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1390680/81e7bef5-e4d1-de47-7ec4-ca4441e84f93.png)


年齢も同様に取得します
![スクリーンショット 2023-11-26 16.36.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1390680/8141815e-a53a-3c5b-c276-91f5856bf562.png)

entryの写し間違いを防ぐためにも、全てのinputで仮データを入れて取得するのが無難です

必要な情報を取得したらGoogle Form右下のフォームをクリアを押してください
![スクリーンショット 2023-11-26 16.41.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1390680/efd81a39-7fbf-b987-2ab5-310b2afb7052.png)

---
FormFieldNamesのkeyはregisterで定義したものにあわせてください
```js
const FormFieldNames = {
  name: "entry.587594010",
  age: "entry.414111699",
};
```
```js
<form onSubmit={handleSubmit(onSubmit)}>
  // ...省略
  <input type="text" {...register("name")} />
  // ...省略
  <input type="text" {...register("age")} />
  // ...省略
</form>
```

## 動作確認
Google Formの連携はこれで完了です。
動作確認の前にGoogle Formに閲覧制限がかかっていないか確認します。
Google FormプレビューのURLをクリップボードにコピーし、ブラウザのシークレットモードで閲覧可能か確認してください。
閲覧できない場合は、閲覧制限がかけられているので、リンクを知っている人全員がGoogle Formを閲覧できるよう変更してください。

---
`npm run dev`でサーバーを起動してください。

なにも入力せずに送信ボタンを押した場合
![スクリーンショット 2023-11-26 16.55.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1390680/83b20df1-7ddc-dd3c-70fe-b29437f85da0.png)

名前だけを入力した場合
![スクリーンショット 2023-11-26 16.55.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1390680/ae0e7e58-91c5-9e55-75d8-8a0fad043ff1.png)

どちらもvalidationが機能しています。

名前と年齢を記入し送信ボタンを押します。
Google Formの回答を確認すると、データが登録されています
![スクリーンショット 2023-11-26 16.57.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1390680/3605bd0f-90bc-4337-3f9d-72d90ef5f2e4.png)

ここまででReact Form Hookを利用したGoogle Form連携は完了です。

## 注意点
実装にあたっては下記の注意点を考慮した上で作成してください
```
fetch後に返されるstatusは常に0 (下記2点の場合ユーザー側は送信に失敗していることに気づけない)
1 google側のサーバーの問題 (滅多にない)
2 誤ってGoogle Formを削除した場合や必須項目をGoogle Formにのみ追加した場合 
(管理側が間違えなければ問題ない)


Wi-Fi(ネットワーク)非接続状態の検知はtry{}catch(){},async,awaitの使用で検知可能
カフェのFree Wi-Fiで1時間経過後通信が遮断されたタイミングで送信ボタンが押された場合など...
```

## text以外の取り扱い
今回はtextのみ扱いましたが、checkboxやradioも同様に作成することが可能です。
下記記事はHTML, CSS, JavaScriptでGoogle Formと連携する方法です。
記事の中ではcheckbox, radio, select等も扱っています。参考にしてください。

https://qiita.com/kandalog/items/867263cf7c1ab0324710

## 終わりに
React Form HookもZodも触ったことがない状態で、土日休暇を利用して実装してみました。
Google Form連携を行わない場合でもReact Form Hookは有用なので、実装した甲斐がありました。
この記事が誰かのお役に立つと嬉しいです。

## 参考
https://developer.mozilla.org/ja/docs/Web/API/FormData

https://developer.mozilla.org/ja/docs/Web/API/URLSearchParams

https://reffect.co.jp/react/react-hook-form/

https://qiita.com/kandalog/items/867263cf7c1ab0324710