【React】ライブラリの《ステート系機能》と《手続き的取得機能》を区別しよう

## React Hook Form の場合

いきなりですが、下のコードを見てください。

「姓」と「名」の入力フィールドがあって、その下にある「ちゃんと再計算される氏名」に続いて姓と名をつなげた文字列が出力されます。

「ちゃんと再計算される氏名」は、姓および名を入力するたびにキチンと更新されます。

```tsx
"use client";

import { type FC } from "react";
import { useForm } from "react-hook-form";

type ProfileFormValues = {
  familyName: string;
  personalName: string;
};

const ProfileEditPage: FC = () => {
  const {
    watch, //
//    getValues,    
    register,
    handleSubmit,
  } = useForm<ProfileFormValues>({
    defaultValues: { familyName: "", personalName: "" },
  });

  const onSubmit = handleSubmit((values) => {
    window.alert(JSON.stringify(values));
  });

  return (
    <form onSubmit={onSubmit}>
      <div>
        <label>
          姓
          <input {...register("familyName")} />
        </label>
        <label>
          名
          <input {...register("personalName")} />
        </label>
      </div>

      <div>
        <span>ちゃんと再計算される氏名：</span>
        <span>{watch("familyName") + " " + watch("personalName")}</span>
      </div>

      <button type="submit">送信</button>
    </form>
  );
};

export default ProfileEditPage;

```

もし `watch()` を `getValues()` に置き換えたらどうなるでしょうか？ 

フォームが思い通りに動作しなくなります。 `getValues()` は「手続き的」に現在のフォームの値を取得するための機能であり、**《コンポーネントのステートっぽく》は動作してくれない**からです。

`watch()` および `useWatch()` は、フォーム上のフィールドの値が更新されると、それをコンポーネントの状態に反映させる機能を備えていますが、`getValues()` はそのような機能を備えていません。 

逆に、イベントハンドラの関数の中では `getValues()` を使うべきで、 `watch()` を呼び出しても、正しく動作する保証がありません。

:::note
- `watch()`, `useWatch()` は**ステート的**な機能
- `getValues()` は**手続き的に値を取得する**機能
:::

```diff_tsx
-     <div>
-       <span>ちゃんと再計算される氏名：</span>
-       <span>{watch("familyName") + " " + watch("personalName")}</span>
-     </div>     
+     <div>
+       <span>再計算されない氏名：</span>
+       <span>{getValues("familyName") + " " + getValues("personalName")}</span>
+     </div>
```

https://react-hook-form.com/docs/useform/watch

https://react-hook-form.com/docs/useform/getvalues

## React の基本機能に立ち戻ってみよう

さきほどのコードを、React Hook Form を使わず useState と controlled component で実装すると、以下のようになります。

- ステート (familyName, personalName) 
- レンダリングの途中で計算される変数 (fullname)
    - これ自体は再レンダリングの発生と関係なく、再レンダリングに追従するだけですが
- サンプルには登場しないが `useSyncExternalStore() から取得した値` 

が、ちょうど `watch()` と同様に**ステート的**な機能だと考えてよいと思います。

イベントハンドラの中では、〈現在の値〉ではなく、〈State as Snapshot としての値〉を取得することになります。

https://qiita.com/honey32/items/ee8d1577e68b0d58678d


いっぽう、Ref については、`current` プロパティを更新しても再レンダリングを発生させず、レンダリング結果として使用不可能。イベントハンドラの中では〈現在の値を取得〉することが可能なので、 `getValues()` と同様に**手続き的に取得する**機能と考えてよいと思います。

以下のコードの `familyName`, `personalName` をそのまま `useRef` に置き換えても意図通りに状態更新がUIに反映されませんが、これは言うまでもありませんね。

```tsx
"use client";

import { type FC, useState } from "react";

const ComputedExamplePage: FC = () => {
  /** 姓および名の入力欄の値を保持するステート */
  const [familyName, setFamilyName] = useState("");
  const [personalName, setPersonalName] = useState("");

  /** フルネーム、姓または名が変更されるたびに、両者を結合して値を更新する */
  const fullName = familyName + " " + personalName;

  return (
    <form>
      <label>
        姓
        <input
          name="familyName"
          value={familyName}
          onChange={(e) => setFamilyName(e.target.value)}
        />
      </label>
      <label>
        名
        <input
          name="personalName"
          value={personalName}
          onChange={(e) => setPersonalName(e.target.value)}
        />
      </label>

      <div>
        <span>氏名</span>
        <span>{fullName}</span>
      </div>

      <div>
        <button type="submit">送信</button>
      </div>
    </form>
  );
};

export default ComputedExamplePage;

```

## SWR (および TanStack Query) の場合

まず、SWR の `useSWR` を利用すると、以下のように、data を《ステート的に》扱うことができます。

```tsx
"use client";

import { FC } from "react";
import useSWR from "swr";

type PostData = {
  userId: string;
  id: string;
  title: string;
  body: string;
};

const QueryPage: FC = () => {
  const { data } = useSWR<PostData[]>(
    "https://jsonplaceholder.typicode.com/posts",
    (url: string) => fetch(url).then((res) => res.json())
  );

  return (
    <div>
      <h1>Posts</h1>
      <hr />
      {data?.map((item) => (
        <div key={item.id}>
          <h2>{item.title}</h2>
          <div>{item.body}</div>
          <hr />
        </div>
      ))}
    </div>
  );
};

export default QueryPage;

```

`useSWR` で注意するべきなのは、**手続き的に取得するのには使用できない**という点です。

以下のような、よくある《郵便番号を入力して、表示された候補の一つを選ぶと、それで入力欄を埋めてくれる》機能を考えてみましょう。

![郵便番号6180000が入力されて、京都府乙訓郡大山崎町と大阪府三島郡島本町の2つの候補が表示されている](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234029/3af64b11-267c-2848-1d82-4440c444bfef.png)

ソースコードの重要な部分の抜粋を以下に示します（JSXやimport等は、この下の「ソースコード全文はこちら」を開くと確認できます。）



```tsx
const AddressCompletionPage: FC = () => {
  // 各フィールドに入力中の文字列
  const [postalCode, setPostalCode] = useState("");
  const [addressPrefecture, setAddressPrefecture] = useState("");
  const [addressArea, setAddressArea] = useState("");
  const [addressDetail, setAddressDetail] = useState("");

  // 郵便番号に対する候補一覧に関する状態、処理
  const [candidates, setCandidates] = useState<Candidate[]>([]);
  const { trigger, isMutating: isFetchingCandidates } = useSWRMutation(
    "https://zipcloud.ibsnet.co.jp/api/search",
    fetcher,
    {
      onSuccess: (data) => setCandidates(data.results ?? []),
    }
  );
  const fetchAndListupCandidates = () => {
    trigger({ postalCode });
  };
  // 選択された候補で、住所を入力補完する
  const completeAddress = (candidate: Candidate) => {
    setAddressPrefecture(candidate.address1);
    setAddressArea(candidate.address2 + " " + candidate.address3);
    setCandidates([]);
  };

// JSX は略  
```

<details><summary>ソースコード全文はこちら</summary>

注意: フォームのマークアップは適当なので、ブラウザのオートコンプリート機能に対応していません。

```tsx
"use client";

import { FC, useState } from "react";
import useSWRMutation from "swr/mutation";

type ZipAddressCandidatesResponse = {
  message: string | null;
  results: Candidate[] | null;
  status: number;
};

type Candidate = {
  address1: string;
  address2: string;
  address3: string;
};

const fetcher = (
  s: string,
  { arg: { postalCode } }: { arg: { postalCode: string } }
): Promise<ZipAddressCandidatesResponse> =>
  fetch(`${s}?zipcode=${postalCode}`).then((res) => res.json());

const AddressCompletionPage: FC = () => {
  // 各フィールドに入力中の文字列
  const [postalCode, setPostalCode] = useState("");
  const [addressPrefecture, setAddressPrefecture] = useState("");
  const [addressArea, setAddressArea] = useState("");
  const [addressDetail, setAddressDetail] = useState("");

  // 郵便番号に対する候補一覧に関する状態、処理
  const [candidates, setCandidates] = useState<Candidate[]>([]);
  const { trigger, isMutating: isFetchingCandidates } = useSWRMutation(
    "https://zipcloud.ibsnet.co.jp/api/search",
    fetcher,
    {
      onSuccess: (data) => setCandidates(data.results ?? []),
    }
  );
  const fetchAndListupCandidates = () => {
    trigger({ postalCode });
  };
  // 選択された候補で、住所を入力補完する
  const completeAddress = (candidate: Candidate) => {
    setAddressPrefecture(candidate.address1);
    setAddressArea(candidate.address2 + " " + candidate.address3);
    setCandidates([]);
  };

  return (
    <form>
      <h2>住所入力</h2>
      <div>
        <input
          name="postalCode"
          placeholder="郵便番号"
          value={postalCode}
          onChange={(ev) => setPostalCode(ev.target.value)}
        />
        <button
          type="button"
          onClick={fetchAndListupCandidates}
          disabled={isFetchingCandidates}
        >
          住所を検索
        </button>
      </div>

      {candidates.length > 0 && (
        <div>
          {candidates
            .map((candidate) => ({
              ...candidate,
              full:
                candidate.address1 + candidate.address2 + candidate.address3,
            }))
            .map((candidate) => (
              <div key={candidate.full}>
                {candidate.full}
                <button
                  type="button"
                  onClick={() => completeAddress(candidate)}
                >
                  これ
                </button>
              </div>
            ))}
          <hr />
        </div>
      )}

      <input
        name="addressPref"
        placeholder="都道府県"
        value={addressPrefecture}
        onChange={(e) => setAddressPrefecture(e.target.value)}
      />

      <input
        name="addressArea"
        placeholder="市町村区"
        value={addressArea}
        onChange={(e) => setAddressArea(e.target.value)}
      />

      <input
        name="addressDetail"
        placeholder="番地・建物名等"
        value={addressDetail}
        onChange={(e) => setAddressDetail(e.target.value)}
      />
    </form>
  );
};

export default AddressCompletionPage;


```
</details>


**データの取得に `useSWR` ではなく `useMutation` を使用している**ことに注目してください。

「住所を検索」ボタンを押下したときに呼び出される関数 `fetchAndListupCandidates` の中で、`fetch` が直接呼び出されていることが分かると思います。

今回の例において、「郵便番号にもとづいて住所の候補を取得してくる」という処理のキッカケが《ページを表示したこと》ではなく、《ユーザーからの入力を受け付けたこと》なので、このように書くのが正解です。

もし、このようなケースで `useSWR` でデータを取得しようとすると、「《ボタンを押したかどうか》のようなBooleanステートが必要になり、何か遠回りなロジックになる」とか「候補が表示されてるときに郵便番号を書き換えると、候補も再取得されてしまう」みたいに、**おかしな挙動と、それを抑えるために書かれたおかしなコード**が襲ってくることになります。

「GET リクエストによるデータ取得だから `useSWR` (または TanStack Query の `useQuery`など)を使おう」と短絡的に考えるのはダメです。**データ取得が何に起因して実行されるのか**に注目しましょう。 公式にも書かれている通りです。（「エフェクト」を「`useSWR`」と読み替えましょう。）

> **あるコードがエフェクトにあるべきか、イベントハンドラにあるべきかわからない場合は、そのコードが実行される <i>理由</i> を自問してください。コンポーネントがユーザに<i>表示されたために</i>実行されるべきコードにのみエフェクトを使用してください。** この例では、通知はユーザが<i>ボタンを押した</i>ために表示されるのであって、ページが表示されたためではありません！ 
>
> https://ja.react.dev/learn/you-might-not-need-an-effect#sharing-logic-between-event-handlers

:::note
SWR の `useSWR`、 TanStsack Query の `useQuery` は **取得したデータをステート的に扱う** ための機能です。そうでないデータ取得には適しません。

手続き的な取得には、（たとえ GET であっても） SWR なら `useSWRMutation`、 TanStack Query なら `useMutation` を使いましょう。
:::

https://swr.vercel.app/ja/docs/data-fetching

https://swr.vercel.app/ja/docs/mutation


https://tanstack.com/query/latest/docs/framework/react/reference/useQuery

https://tanstack.com/query/latest/docs/framework/react/reference/useMutation



## Recoil の場合は

Recoil については、react-hook-form, SWR に比べて、間違えたときのダメージがさほど大きくなさそうなので、軽く流しますが、以下のように分類できます。

- `useRecoilValue`, `useRecoilState` は**ステート系**の機能です
- `useRecoilCallback` の中では、**現在の値**を取得できます

https://recoiljs.org/docs/api-reference/core/useRecoilState

https://recoiljs.org/docs/api-reference/core/useRecoilValue

https://recoiljs.org/docs/api-reference/core/useRecoilCallback

## Jotai の場合

Recoil とコンセプトが似ている Jotai についても、同様です。

- `useAtom`, `useAtomValue` は**ステート系**の機能です
- `useAtomCallback`(および `atom()` のセッター関数) の中では、**現在の値**を取得できます
    - `atom()` に渡すセッター関数も似たようなものです

https://jotai.org/docs/core/use-atom

https://jotai.org/docs/utilities/callback