useSWRの基本的な使い方をまとめた

# useSWRとは

API通信に対して、レスポンス情報をキャッシュすることで、毎回API通信がされてしまうのを防ぐhooks。
公式によると、以下のようなメリットがある。

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

# 使い方

### 公式ドキュメント

[はじめに – SWR](https://swr.vercel.app/ja/docs/getting-started)

### 基本的な使い方

```tsx
import useSWR from 'swr';

type User = {
  id: string;
  name: string;
  email: string;
}

async function fetcher(key: string) {　// keyはuseSWR()の第１引数で渡されたURL
  return fetch(key).then((res) => res.json() as Promise<User | null>);
}

export const App = () => {

  const { data, error, isLoading } = useSWR('https://jsonplaceholder.typicode.com/users/1', fetcher);

  if (error) return <div>エラーです</div>;
  if(isLoading) return <div>読み込み中...</div>;

  return (
    <div>
      <h1>名前：{data?.name}</h1>
      <h1>メール：{data?.email}</h1>
    </div>
  );
}
```

`data`には`fetcher`の返り値が入る。同時に、`error`と現在読み込み中かを表す`isLoading`を取得できるので、それぞれがtrueのときは別の画面を表示するようにしてある。
公式ドキュメントのクイックスタートでは`fetcher`は非同期関数になっていないが、実際に利用する際は上記の例のように、非同期関数にすることを推奨する（`fetcher`は結局のところネイティブの `fetch` をラップした関数なので、いつも`fetch`を使うノリでPromiseを返すようにしておきましょう）。

`fetcher`は以下のように定義することもできる。以下3つとも同じ動作をする。

```jsx
useSWR('/api/user', () => fetcher('/api/user'))
useSWR('/api/user', url => fetcher(url))
useSWR('/api/user', fetcher)
```

`fetcher`は最終的に必要なデータを取得して適切な形式で渡す非同期関数であればいいので、ネイティブの`fetch`を使う必要はなく、`axios`などを利用することもできる。

```tsx
import axios from 'axios'
 
const fetcher = url => axios.get(url).then(res => res.data)
```

### データの事前fetch

上記のコードでは該当するコンポーネントを表示しない限り、データのfetchは当然行われない。だが`preload`を使うと事前にデータをfetchしておく、ということもできる。

```tsx
import useSWR from 'swr';
import { preload } from 'swr';

type User = {
  id: string;
  name: string;
  email: string;
}

async function fetcher(key: string) {　// keyはuseSWR()の第１引数で渡されたURL
  return fetch(key).then((res) => res.json() as Promise<User | null>);
}

// データの事前読み込み（当然キャッシュに保存される）
plelaod('https://jsonplaceholder.typicode.com/users/1', fetcher);

export const App = () => {

  const { data, error, isLoading } = useSWR('https://jsonplaceholder.typicode.com/users/1', fetcher);

  if (error) return <div>エラーです</div>;
  if(isLoading) return <div>読み込み中...</div>;

  return (
    <div>
      <h1>名前：{data?.name}</h1>
      <h1>メール：{data?.email}</h1>
    </div>
  );
}
```

### hooksにまとめる

各component全てに`fetcher`を書くのは面倒なので、hooksとしてまとめるのが良い。

基本的な使い方で示した例をhooksに分割すると以下のようになる。
（以降の例では`preload`は除いてある）

```tsx
// App.tsx
import { useUsers } from './hooks/useUsers';

export const App = () => {

  const { user, isLoading, isError } = useUsers(1);

  if (isError) return <div>エラーです</div>;
  if(isLoading) return <div>読み込み中...</div>;

  return (
    <div>
      <h1>名前：{user?.name}</h1>
      <h1>メール：{user?.email}</h1>
    </div>
  );
}
```

```tsx
// useUser.ts
import useSWR from 'swr';

type User = {
  id: string;
  name: string;
  email: string;
}

async function fetcher(key: string) {
  return fetch(key).then((res) => res.json() as Promise<User | null>);
}

export const useUsers = (id: number) => {
    const { data, error, isLoading } = useSWR(`https://jsonplaceholder.typicode.com/users/${id}`, fetcher);

    return {
        user: data,
        isLoading,
        isError: error,
    }
}
```

`user`の情報を取得するロジックを抽象化し、`useUsers.ts`にまとめた。これで他のコンポーネントでuser情報を使いたくなった場合は`const { user, isLoading, isError } = useUsers(1);`のように呼び出せばいい。1行書けばOKになったので、かなり簡単である。

# エラーハンドリング

```tsx
const { data, error, isLoading } = useSWR('hoge', fetcher);
```

`fetcher`でrejectされた場合、`error`オブジェクトに値が入ることになる（errorがない場合はundefined）。
必ずrejectが起きる`fetcher`を作って確認してみる。

```tsx
async function fetcher(key: string) {
  return new Promise<User | null>((resolve, reject) => {
    fetch(key)
      .then((res) => {
        reject(new Error(`errorが発生しました`)); // 必ずreject
      })
  });
}

const { data, error, isLoading } = useSWR(`https://jsonplaceholder.typicode.com/users/${id}`, fetcher);
console.log(error); // errorが発生しました
```

errorが発生しているとき、リクエストの再試行が行われる。再試行には[exponential backoff アルゴリズム](https://note.com/artrigger_jp/n/n0795148b062d)が使われている。これにより、効率的なリクエストの再試行を実現している。
再試行の設定は`onErrorRetry`で上書きすることもできる。

```tsx
useSWR('/api/user', fetcher, {
  onErrorRetry: (error, key, config, revalidate, { retryCount }) => {
    // 404では再試行しない。
    if (error.status === 404) return
 
    // 特定のキーでは再試行しない。
    if (key === '/api/user') return
 
    // 再試行は10回までしかできません。
    if (retryCount >= 10) return
 
    // 5秒後に再試行します。
    setTimeout(() => revalidate({ retryCount }), 5000)
  }
})
```

# データの検証

useSWRは取得したデータをキャッシュすることで高速化を実現している。だがキャッシュされたデータが古くなってしまった場合、適切な情報をクライアントに渡すことができなくなってしまう。そこでuseSWRでは、データの自動検証が行われている。

検証されるタイミングは

- フォーカスしたとき再検証
- 設定された時間時間が経過したら再検証
- ユーザーがオンラインに戻ったとき再検証

などがある

### フォーカスしたとき再検証

タブを切り替えた時などにデータを検証する。以下がビジュアル的にもわかりやすい。

[自動再検証 – SWR](https://swr.vercel.app/ja/docs/revalidation#revalidate-on-focus)

### 設定された時間が経過したら再検証

ある程度の時間が経過したらデータを検証して、必要に応じて再びデータのfetchを行うことができる。検証対象はuseSWRで管理されているデータのうち、現在表示されているコンポーネントに関係があるもの。それ以外は検証の対象外になる。
設定は以下の通り。

```tsx
const { data, error, isLoading } = useSWR(
	`https://jsonplaceholder.typicode.com/users/${id}`,
	fetcher,
	{
		refreshInterval: 1000, // 検証時間(ms)。デフォルトでは無効。
	},
);
```

### ユーザーがオンラインに戻ったとき再検証

ユーザーがコンピューターのロックを解除したときなどに自動的に再検証を行う。

# キャッシュのカスタマイズ

デフォルトでは、 SWR はグローバルキャッシュを使用して、すべてのコンポーネント間でデータを保存および共有する。

ただ、`SWRConfig` の `provider` オプションを使うことでこれをカスタマイズすることができる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3527059/615c8664-4f82-1e40-655c-817f0ceb6c09.png)


https://swr.vercel.app/ja/docs/advanced/cache

上記のように、キャッシュをグローバルではないものとして扱うことができる。

providerの生成は以下のように行う。

```tsx
import useSWR, { SWRConfig } from 'swr'
 
function App() {
  return (
    <SWRConfig value={{ provider: () => new Map() }}>
      <Page/>
    </SWRConfig>
  )
}
```

# キャッシュの更新（ミューテーション）

`mutate`を使うことで、データのfetchを行わずにキャッシュを更新することができる。addやeditを行ったとき、わざわざfetchしなくてもキャッシュを更新できるので便利。reduxでいうグローバルstateの更新だと考えるとわかりやすい。reduxでいうstateが、useSWRだとキーというものになる。キーは具体的にはfetcherに渡したエンドポイント（例えば`https://jsonplaceholder.typicode.com/users/1`）である。useSWRではfetchを実行する前に、キーに対応するキャッシュがある場合は、これを返すことになる。

### バウンドミューテート

現在のキーのデータをmutateする。以下は「更新」ボタンをクリックすると`mutate`の第１引数に渡しているオブジェクトの内容にキャッシュを更新する例である。

`mutate`の第２引数にfalseを設定しないとキャッシュの再検証が行われる。つまりデータのfetchが自動的にされてしまう。キャッシュ更新後に再検証を行うことで、キャッシュのデータが正しいことが保証されるというメリットがあるが、意図せず通信回数を増やしてしまわないよう、適宜falseを設定しておきたい。

```tsx
// usePosts.ts
export const usePosts = (id: number) => {
  const { data, error, isLoading, mutate } = useSWR(`https://jsonplaceholder.typicode.com/posts/${id}`, fetcher);

  return {
    post: data,
    isLoading,
    isError: error,
    mutate, // mutateをApp.tsxで呼び出せるようにする
  }
}
```

```tsx
// App.tsx
import { usePosts } from './hooks/usePosts';
import { updatePost } from 'hoge';

export const App = () => {
  const { post, mutate } = usePosts(1);

  const onClick = async () => {
    const newPost = {
      id: 1,
      title: '更新後のタイトル',
      body: '更新後の本文',
      userId: 1,
    }
    await updatePost(newPost); // postを更新する仮のメソッド
    mutate(newPost, false); // `https:/...com/posts/1`に紐づくキャッシュを更新する
  }

  return (
    <div>     
      <p>タイトル：{post?.title}</p>
      <p>本文：{post?.body}</p>
      <button onClick={onClick}>更新</button>
    </div>
  );
}
```

### グローバルミューテート

全てのキーに対して更新可能な`mutate`を定義することができる。使い方は以下の通り。

```tsx
import { useSWRConfig } from "swr"
 
export const App = () => {
  const { mutate } = useSWRConfig();
  mutate(key, data, options);
}
```

これまでの例の中で使ってみると、

```tsx
import { usePosts } from './hooks/usePosts';
import { updatePost } from 'hoge';
import { useSWRConfig } from 'swr';

export const App = () => {

  const id = 1;
  const { post } = usePosts(id);
  const { mutate } = useSWRConfig(); // グローバルなmutate

  const onClick = async () => {
    const newPost = {
      id: 1,
      title: '更新後のタイトル',
      body: '更新後の本文',
      userId: 1,
    }
    await updatePost(newPost); // postを更新する仮の非同期関数

    // 引数は　　キー, 更新内容, 再検証を行うかどうか　（falseで行わない）　
    mutate(`https://jsonplaceholder.typicode.com/posts/${id}`,　newPost, false);
  }

  return (
    <div>   
      <p>タイトル：{post?.title}</p>
      <p>本文：{post?.body}</p>
      <button onClick={onClick}>更新</button>
    </div>
  );
}
```

一つの`mutate`で色々なキーのキャッシュについて更新ができるので便利ではあるが、第１引数に確実にキーを指定しなければならないので注意が必要。

# なにがどうなったら再fetchが起きる？

再検証が行われるタイミングは上記の通りであるが、そもそも何を持って再fetchが必要と判断しているのかというと、更新前後のデータについて浅い比較を行い、変更がなければfetchしないと判断している。

```tsx
// usePosts.ts
import useSWR from 'swr';

type Post = {
  id: number;
  title: string;
  body: string;
  userId: number;
}

async function fetcher(key: string) {
  return fetch(key).then((res) => res.json() as Promise<Post | null>);
}
  
export const usePosts = (id: number) => {
  const { data, error, isLoading, mutate } = useSWR(`https://jsonplaceholder.typicode.com/posts/${id}`, fetcher);
  console.log('実行'); // consoleに実行と表示されないことがわかる

  return {
    post: data,
    isLoading,
    isError: error,
    mutate,
  }
}
```

```tsx
// App.tsx
import { useEffect } from 'react';
import { usePosts } from './hooks/usePosts';
import { useUsers } from './hooks/useUsers';

import { useSWRConfig } from 'swr';

export const App = () => {

  const id = 1;
  const { post } = usePosts(id);
  const { user, isLoading, isError } = useUsers(post?.userId);
  const { mutate } = useSWRConfig();

  const onClick = async () => {
    // fetchを実行する前に更新前後のデータを比較（浅い比較？）していて、変更がなければfetcherは実行されない
	// このnewPostは現在のpostと全く同じなので、再fetchはされない
	const newPost = {
      id: post?.id,
      title: post?.title,
      body: post?.body,
      userId: post?.userId,
    }

    mutate(`https://jsonplaceholder.typicode.com/posts/${id}` ,newPost, false);
  }

  if (isError) return <div>エラーです</div>;
  if(isLoading) return <div>読み込み中...</div>;

  return (
    <div>
      <h1>名前：{user?.name}</h1>
      <h1>メール：{user?.email}</h1>      
      <p>タイトル：{post?.title}</p>
      <p>本文：{post?.body}</p>
      <button onClick={onClick}>更新</button>
    </div>
  );
}
```

# サブスクリプション

（調べてもあまり情報が出てこなかったので間違えている可能性が高いです・・・）

`useSWRSubscription`を使うことで、サブスクリプションを実現する。
サブスクリプションとは、クライアントがサーバーをリアルタイムで監視し、サーバー側で変更があったらこれを検知して何らかの処理を行うことである。

（今後学習が進んだら追記する予定です。）

# 応用的な使い方

### 条件付きfetch

第一引数にnullもしくはfalsyな値を渡すことができる。その場合はfetchは実行されず、dataはundefinedのままになる。

```tsx
// 条件付きでフェッチする
const { data } = useSWR(shouldFetch ? '/api/data' : null, fetcher)
 
// ...または、falsyな値を返します
const { data } = useSWR(() => shouldFetch ? '/api/data' : null, fetcher)
 
// ...または、user.id が定義されてない場合にスローします
const { data } = useSWR(() => '/api/data?uid=' + user.id, fetcher)
```

### 依存関係のあるfetch

条件付きfetchを利用して、依存関係のあるfetchを簡単に実装することができる。

```tsx
// App.tsc
import { usePosts } from './hooks/usePosts';
import { useUsers } from './hooks/useUsers';

export const App = () => {

  const { post } = usePosts(1);
  const { user, isLoading, isError } = useUsers(post?.userId); // postの取得が完了してからfetchされる

  if (isError) return <div>エラーです</div>;
  if(isLoading) return <div>読み込み中...</div>;

  return (
    <div>
      <h1>名前：{user?.name}</h1>
      <h1>メール：{user?.email}</h1>      
      <p>タイトル：{post?.title}</p>
      <p>本文：{post?.body}</p>
    </div>
  );
}
```

```tsx
// usePosts.ts
import useSWR from 'swr';

type Post = {
  id: number;
  title: string;
  body: string;
  userId: number;
}

async function fetcher(key: string) {
  return fetch(key).then((res) => res.json() as Promise<Post | null>);
}
  
export const usePosts = (id: number) => {
  const { data, error, isLoading } = useSWR(`https://jsonplaceholder.typicode.com/posts/${id}`, fetcher);

  return {
    post: data,
    isLoading,
    isError: error,
  }
}
```

```tsx
// useUsers.ts
import useSWR from 'swr';

type User = {
  id: string;
  name: string;
  email: string;
}

async function fetcher(key: string) {
  return fetch(key).then((res) => res.json() as Promise<User | null>);
}

export const useUsers = (id: number | undefined) => {
  const { data, error, isLoading } = useSWR(
    id ? `https://jsonplaceholder.typicode.com/users/${id}` : null, // idがundefinedならfetchしない
    fetcher
  );

  return {
      user: data,
      isLoading,
      isError: error,
  }
}
```

# 個人的な不満点

reduxをやめてuseSWRを採用する、となったとき、コンポーネント間で利用したいstateをどう扱うのかを検討する必要がある。useSWRはfetchに関する部分に焦点を当てたhooks（だと感じた）ため、データのfetchを伴わないstate（現在モーダルが開いているかなど）の扱いは、useReducerなどグローバルstateを扱うものを結局のところ導入しないといけない。

ここさえどうにかなればなぁ・・・。

# 参考
https://swr.vercel.app/ja

https://zenn.dev/uttk/articles/b3bcbedbc1fd00

https://zenn.dev/mast1ff/articles/5b48a87242f9f0

https://qiita.com/yuuki_0524/items/b8a863b0222ac11bb65a