SWRとReactのカスタムフックを使ってバケツリレー問題を解消する

## はじめに
Componentのバケツリレー問題をどのように解決すればいいかずっと悩んでました😅そんな問題を解決してくれる素晴らしいライブラリがありました。**SWR**です！！自分が抱えていたバケツリレーの問題をすぐ解決してくれました😊

今回は、SWRを使ってTodoアプリの状態管理の方法を解説していきます！完成した時のイメージがこちらです。

<iframe width="560" height="315" src="https://www.youtube.com/embed/e6PwtIGGUmY" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>


**実行環境**
* Next.js : 12.1.0
* React : 17.0.2
* TypeScript : 4.6.2
* TailwindCSS : 3.0.23
* SWR : 1.2.2

## SWRとは？
SWRは**データ取得のための React Hooks ライブラリ**です。
### SWRの特徴
**たった 1 行のコードで、プロジェクト内のデータ取得のロジックを単純化し、さらにこれらの素晴らしい機能をすぐに利用できるようになります。**

* 速い、 軽量 そして 再利用可能 なデータの取得
* 組み込みの キャッシュ とリクエストの重複排除
* リアルタイム な体験
* トランスポートとプロトコルにとらわれない
* SSR / ISR / SSG support
* TypeScript 対応
* React Native

**SWR は、スピード、正確性、安定性のすべての面をカバーし、より良い体験を構築するのに役立ちます。**

* 高速なページナビゲーション
* 定期的にポーリングする
* データの依存関係
* フォーカス時の再検証
* ネットワーク回復時の再検証
* ローカルキャッシュの更新（Optimistic UI）
* スマートなエラーの再試行
* ページネーションとスクロールポジションの回復
* React Suspense


>https://swr.vercel.app/ja

でも、データ取得のためのライブラリなのにどうして状態管理ができるかのか？
それは、APIのリクエストをせずに状態が保持できるのが理由です。Shuさんという方が思いついたらしいです。

>https://paco.me/writing/shared-hook-state-with-swr

### 全体のコード
```ts: pages/index.tsx
import type { NextPage } from 'next';
import { Card } from '@/component/Card';
import { Form } from '@/component/Form';
import { useTodo } from '@/hook/useTodo';

const Home: NextPage = () => {
  const { todo } = useTodo();
  return (
    <div className='my-12 text-center'>
      <Form />
      <div className='grid grid-cols-2 justify-items-center mt-4'>
        {todo.map((t, index) => (
          <div key={index} className='mt-4'>
            <Card title={t.title} task={t.task} i={index} />
          </div>
        ))}
      </div>
    </div>
  );
};

export default Home;
```


```ts: component/Form.tsx
import React from 'react';
import { useTodo } from '@/hook/useTodo';

export const Form: React.FC = () => {
  const { form, isUpdate, setForm, handleCreate, handleUpdate } = useTodo();

  return (
    <div>
      <input
        type='text'
        placeholder='Task'
        value={form.title}
        onChange={(e) => setForm({ ...form, title: e.target.value })}
        className='px-1 rounded-full border border-gray-300'
      />
      <input
        type='text'
        placeholder='Todo'
        value={form.task}
        onChange={(e) => setForm({ ...form, task: e.target.value })}
        className='px-1 mx-6 rounded-full border border-gray-300'
      />
      {isUpdate === false ? (
        <button onClick={handleCreate} className='py-0.5 px-2 text-white bg-blue-500 rounded-lg'>
          作成
        </button>
      ) : (
        <button onClick={handleUpdate} className='py-0.5 px-2 text-white bg-pink-500 rounded-lg'>
          更新
        </button>
      )}
    </div>
  );
};
```

```ts: component/Card.tsx
import React from 'react';
import { useTodo } from '@/hook/useTodo';

interface CardProps {
  title: string;
  task: string;
}

export interface indexProps {
  i: number;
}

export const Card: React.FC<CardProps & indexProps> = ({ title, task, i }) => {
  const { handleEdit, handleDelete } = useTodo();

  return (
    <div className='py-4 w-[400px] rounded-lg border border-gray-300 shadow-md'>
      <h1 className='text-3xl font-bold'>{title}</h1>
      <p className='px-2 my-4 text-left'>{task}</p>
      <div className='flex justify-end '>
        <button
          onClick={() => handleEdit(i)}
          className='py-0.5 px-2 mr-2 text-white bg-green-500 rounded-lg'
        >
          編集
        </button>
        <button
          onClick={() => handleDelete(i)}
          className='py-0.5 px-2 mr-2 text-white bg-red-500 rounded-lg'
        >
          削除
        </button>
      </div>
    </div>
  );
};
```
```ts: hook/useStateSWR.tsx
import useSWR from 'swr';

interface TodoProps {
  title: string;
  task: string;
}

export const useFormSWR = (
  key: string,
  initialData: TodoProps,
): [TodoProps, (state: TodoProps) => void] => {
  const { data: state, mutate: setState } = useSWR(key, null, { fallbackData: initialData });
  return [state as TodoProps, setState];
};

export const useTodoSWR = (
  key: string,
  initialData: TodoProps[],
): [TodoProps[], (state: TodoProps[]) => void] => {
  const { data: state, mutate: setState } = useSWR(key, null, { fallbackData: initialData });
  return [state as TodoProps[], setState];
};

export const useIndexSWR = (
  key: string,
  initialData: number,
): [number, (state: number) => void] => {
  const { data: state, mutate: setState } = useSWR(key, null, { fallbackData: initialData });
  return [state as number, setState];
};

export const useUpdateSWR = (
  key: string,
  initialData: boolean,
): [boolean, (state: boolean) => void] => {
  const { data: state, mutate: setState } = useSWR(key, null, { fallbackData: initialData });
  return [state as boolean, setState];
};
```

```ts: hook/useTodo.tsx
import { useFormSWR, useTodoSWR, useIndexSWR, useUpdateSWR } from '@/hook/useStateSWR';

export const useTodo = () => {
  const [form, setForm] = useFormSWR('form', { title: '', task: '' });
  const [todo, setTodo] = useTodoSWR('todo', []);
  const [index, setIndex] = useIndexSWR('index', 0);
  const [isUpdate, setIsUpdate] = useUpdateSWR('isUpdate', false);

  const handleCreate = () => {
    setTodo([...todo, form]);
    setForm({ title: '', task: '' });
  };

  const handleUpdate = () => {
    const updateTodo = [...todo];
    updateTodo.splice(index, 1, form);
    setTodo(updateTodo);
    setIsUpdate(false);
    setForm({ title: '', task: '' });
  };

  const handleEdit = (i: number) => {
    setIndex(i);
    setForm(todo[i]);
    setIsUpdate(true);
  };

  const handleDelete = (i: number) => {
    const deleteTodo = [...todo];
    deleteTodo.splice(i, 1);
    setTodo(deleteTodo);
    setForm({ title: '', task: '' });
    setIsUpdate(false);
  };

  return {
    form,
    todo,
    index,
    isUpdate,
    setForm,
    setTodo,
    setIndex,
    setIsUpdate,
    handleCreate,
    handleEdit,
    handleUpdate,
    handleDelete,
  };
};
```

### 他のファイルでも共有ができるstate変数を設定する
``useStateSWR.tsx``で他のファイルでも共有ができるstate変数を設定します。

今回用意していくのは以下の4つを設定してます。
* **フォームに入力されたデータを作成と更新をするためのstate変数**
* **作成されたTodoリストを表示させるCardのstate変数**
* **Cardの編集ボタンを押したときはtrue、更新と削除ボタンを押したときはfalseを返すためのstate変数**
* **Cardの編集ボタンと削除ボタンを押したときに配列番号を取得するためのstate変数**


それぞれのTypeについては類似しているので、``useIndexSWR``だけを説明します。コードの意味は``const [state, setState] = useState(0)``と同じです。
```ts
export const useIndexSWR = (
  key: string,
  initialData: number,
): [number, (state: number) => void] => {
  const { data: state, mutate: setState } = useSWR(key, null, { fallbackData: initialData });
  return [state as number, setState];
};
```
initialDataについてはnumber型で設定してあげないといけないので、numberで設定してます。``useIndexSWR``はCardコンポーネントを配列で返しており、編集ボタンと削除ボタンを押したときにその配列番号を保持するためようの変数なのでnumber型で設定してます。
``useSWR``の第二引数をnullで明示的に設定することでAPIリクエストは行わないようにする。

:::note warn
useSWRの設定について調べると第三引数のfallbackDataがinitialDataの名前で書かれていた記事しかなかったです。現在useSWRの第三引数のOptionにはinitialDataの引数はないです。
SWRの公式ドキュメントではfallbackDataの引数が初期データを返す引数となってます。
> https://swr.vercel.app/docs/options
:::


### カスタムフックで設定したstate変数を使ってロジックを書いていく
``useStateSWR.tsx``で設定したstate変数は状態保持したまま他のファイルでも使用できるようになっています。なのでサイトUIのプレゼンテーションとビジネスロジックを切り離してコードを書くことができます。

``useTodo.tsx``でstate変数の初期設定とTodoアプのCRUD処理を書いていきます。
```ts
import { useFormSWR, useTodoSWR, useIndexSWR, useUpdateSWR } from '@/hook/useStateSWR';

export const useTodo = () => {
  const [form, setForm] = useFormSWR('form', { title: '', task: '' });
  const [todo, setTodo] = useTodoSWR('todo', []);
  const [index, setIndex] = useIndexSWR('index', 0);
  const [isUpdate, setIsUpdate] = useUpdateSWR('isUpdate', false);

  const handleCreate = () => {
    setTodo([...todo, form]);
    setForm({ title: '', task: '' });
  };

  const handleUpdate = () => {
    const updateTodo = [...todo];
    updateTodo.splice(index, 1, form);
    setTodo(updateTodo);
    setIsUpdate(false);
    setForm({ title: '', task: '' });
  };

  const handleEdit = (i: number) => {
    setIndex(i);
    setForm(todo[i]);
    setIsUpdate(true);
  };

  const handleDelete = (i: number) => {
    const deleteTodo = [...todo];
    deleteTodo.splice(i, 1);
    setTodo(deleteTodo);
    setForm({ title: '', task: '' });
    setIsUpdate(false);
  };

  return {
    form,
    todo,
    index,
    isUpdate,
    setForm,
    setTodo,
    setIndex,
    setIsUpdate,
    handleCreate,
    handleEdit,
    handleUpdate,
    handleDelete,
  };
};
```

あとはそれぞれのファイル（``Form.tsx, Card.tsx, index.tsx``）にカスタムフックで設定したstate変数やビジネスロジックを渡してあげれば完成です。

## 最後に
SWRサイコーです😊SWRのライブラリのおかげでバケツリレー問題はすんなり解決できます！！
この記事が少しでも参考になれば幸いです。

最後までお読みいただきありがとうございました。