react-queryのカスタムフックを作ってmswを使ったテストを書く

## react-queryとは
react-query(tanstack query)はReactを利用する際、データの取得やキャッシュを用いた状態の管理を便利に行える多機能なライブラリです。

https://github.com/TanStack/query

類似ライブラリには [SWR](https://swr.vercel.app/ja) があります。使い方も機能的にも似てますので合いそうな方を選ぶとよいかと思います。

## react-queryを使うとどう変わるか
react-queryを使わずにfetchやaxiosなどを使った実装を行う場合、データ取得中のローディングや取得時のエラーの状態管理やキャッシュについて考えたりする必要がありますが、react-queryを使うとその辺をシンプルに実装することができます。
以下、データ取得中のローディングの状態管理をreact-queryありなしで実装した場合のコードです。

### react-queryなし

```tsx:UserList.tsx
import axios from "axios";
import { useState, useEffect } from "react";
import { User } from "../types/types";

const fetchUsers = async () => {
  const res = await axios.get("https://jsonplaceholder.typicode.com/users");
  return res.data;
};
  
export const Test = () => {
  const [users, setUsers] = useState<User[]>([]);
  const [isLoading, setIsLoading] = useState<boolean>(true);

  //データを取得 
  useEffect(() => {
    fetchUsers().then((data) => {
      setUsers(data);
      setIsLoading(false);
    });
  }, []);

  if (isLoading) return <div>{"Loading"}</div>;

  return (
    <div>
      <h2>ユーザ一覧</h2>
      <div>
        {users.map((user) => (
          <div key={user.id}>{user.name}</div>
        ))}
      </div>
    </div>
  );
};
```

### react-queryあり

```tsx:UserList.tsx
import { useQuery } from "@tanstack/react-query";
import axios from "axios";
import { User } from "../types/types";

const fetchUsers = async () => {
  const res = await axios.get("https://jsonplaceholder.typicode.com/users");
  return res.data;
};

export const Test = () => {
  const { status, data } = useQuery(["users"], fetchUsers);

  if (status === "loading") return <div>{"Loading"}</div>;

  return (
    <div>
      <h2>ユーザ一覧</h2>
      <div>
        {data.map((user: User) => (
          <div key={user.id}>{user.name}</div>
        ))}
      </div>
    </div>
  );
};
```

useEffectやuseStateによるLoadingの状態管理部分がシンプルになっています。今回はローディングの状態だけですが、エラーやキャッシュ操作などを行っていくときにより効果を発揮します。また、データ取得用のuseQuery以外にもデータ更新用のuseMutationなども用意されています。

https://tanstack.com/query/v4/docs/reference/useMutation

## カスタムフックにする

データ取得の部分が楽になるreact-queryですが、複数コンポーネントで利用されたり、テストを行うためにカスタムフックにして切り出したいと思います。
hooks/useFetchUsers.tsxという名前で作成します。

```tsx:userFetchUsers.tsx
import { useQuery } from "@tanstack/react-query";
import { User } from "../types/types";

const fetchUsers = async () => {
  const res = await axios.get("https://jsonplaceholder.typicode.com/users");
  return res.data;
};

export const useFetchUsers = () => {
  return useQuery<User[], Error>(["users"], fetchUsers );
};
```

先ほどのUserList.tsxから呼び出すときは以下のようになります。

```tsx:UserList.tsx
import React from "react";
import { useFetchUsers } from "../hooks/useUserData";
import type { User } from "../types/types";

export const UserList = () => {
  const { status, data } = useFetchUsers();

  if (status === "loading") return <div>{"Loading"}</div>;
  if (status === "error") return <div>{"Error"}</div>;

  return (
    <>
      {data?.map((user: User) => (
        <div key={user.id}>{user.name}</div>
      ))}
    </>
  );
};
```

## カスタムフックに対してmswを使ったテストを書く
mswはブラウザ、Node環境でRest/Graphqlのリクエストをモックしてくれるライブラリです。詳しくは自分の記事で書いていますのでこちらを参考ください。

https://qiita.com/stake15/items/50476b986c8dab8d7fcf

msw関連のファイルをmocksディレクトリに2つ作ります。（今回はブラウザ用のファイルは作成しません）

```typescript:mocks/handlers.ts
import { rest } from "msw";

export const handlers = [
  rest.get("https://jsonplaceholder.typicode.com/users", (req, res, ctx) => {
    return res(
      ctx.status(200),
      ctx.json([
        {
          id: 1,
          name: "Leanne Graham",
          username: "Bret",
          email: "Sincere@april.biz",
          address: {
            street: "Kulas Light",
            suite: "Apt. 556",
            city: "Gwenborough",
            zipcode: "92998-3874",
            geo: {
              lat: "-37.3159",
              lng: "81.1496",
            },
          },
          phone: "1-770-736-8031 x56442",
          website: "hildegard.org",
          company: {
            name: "Romaguera-Crona",
            catchPhrase: "Multi-layered client-server neural-net",
            bs: "harness real-time e-markets",
          },
        },
      ])
    );
  }),
];
```


```typescript:mocks/server.ts
import { setupServer } from "msw/node";
import { handlers } from "./handlers";

export const server = setupServer(...handlers);
```

hooks/useFetchUsers.tsxをreact-testing-libraryを使ってテストするコードは以下のようになります。wrapperの部分などは[react-query公式ドキュメント](https://tanstack.com/query/v4/docs/guides/testing)を参考にしています。

```tsx:hooks/useFetchUsers.test.tsx
import { server } from "../mocks/server";
import { renderHook, waitFor } from "@testing-library/react";
import { rest } from "msw";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useFetchUsers } from "./useUserData";
import { ReactNode } from "react";

type wrapperType = { children: ReactNode };

describe("useFetchUsers", () => {
  const queryClient = new QueryClient({
    defaultOptions: {
      queries: {
        retry: false,
        cacheTime: 0,
      },
    },
  });
  const wrapper = ({ children }: wrapperType) => (
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  );

  beforeAll(() => server.listen());
  afterEach(() => {
    server.resetHandlers();
    queryClient.clear();
  });
  afterAll(() => server.close());

  test("API通信成功", async () => {
    const { result } = renderHook(() => useFetchUsers(), { wrapper });
    await waitFor(() => expect(result.current.isSuccess).toBe(true));
    expect(result.current.data).toHaveLength(1);
    expect(result.current.isLoading).toBeFalsy();
    expect(result.current.error).toBeNull();
  });

  test("API通信失敗(500エラー)", async () => {
    server.use(
      rest.get(
        "https://jsonplaceholder.typicode.com/users",
        (req, res, ctx) => {
          return res.once(
            ctx.status(500),
            ctx.json({ message: "Internal Server Error" })
          );
        }
      )
    );
    const { result } = renderHook(() => useFetchUsers(), { wrapper });
    await waitFor(() => expect(result.current.isError).toBe(true));
  });
});
```

## 感想
今までreact-queryの存在を知らず、自分で実装していたのでこういうライブラリがあることを知れてよかったです。今回は利用していませんが、キャッシュやページングなどたくさんの機能があるようなので試していきたいです。また、カスタムフックやmswなど今までの知識が組み合わさって出来上がっていくreactのエコシステム体験がよかったです。
自分が勉強するときもここら辺の組み合わせの記事がなかなかなかったので誰かの参考になれば幸いです。