Closed5

【React】業務システム向けのGraphQLクライアントを探している

こんぶ茶こんぶ茶

やっていること

  • Reactで業務システムを開発する際のひな形、デモアプリを作っている

やりたいこと

  • ReactのSuspenseでGraphQLリクエストのローディング状態とエラー状態をハンドリングしたい
  • graphql-codegenでコードを生成して快適に開発したい
  • レンダリングの最中ではなく、任意のタイミングでデータを取得したい
    • 例えば、一覧画面の検索ボタンが押されたタイミングでfetchしたい
    • コンポーネントのレンダリング時にuseQuery的な感じで取得するのは×
      • ただし、urqlやreact-queryのようにfetchを停止できる&fetchするための関数を返してくれるのであれば○
  • swrやreact-queryなどなど様々なライブラリでデフォルト有効になっているキャッシュを無効にしたい
    • 例えば、生産管理システムのような業務システムにキャッシュは必要ない、というかキャッシュしちゃダメ
    • データを取得する際は常にサーバーからfreshなデータを取得する必要がある

サンプルコード

なんとなくこんな感じのことがやりたい。

index.tsx
ReactDOM.render(
  <Suspense fallback={<h1>Loading...</h1>}>
    <UserList />
  </Suspense>,
  document.getElementById("root")
);
UserList.tsx
const UserList = () => {
  const [data, executeQuery] = useQuery(GetUserListQuery);

  const handleSearch = () => {
    // 要件(1) ここで初めてデータを取得しにいくこと
    // 要件(2) executeQuery実行後にSuspenseが効くこと(ローディング状態になること)
    // 要件(3) どんな状況であろうとも、必ずサーバーにデータを取得しにいくこと
    executeQuery();
  }

  return (
    <>
    <button onClick={handleSearch}>検索</button>
    <ul>
      {data.users.map((user) => <li>{user.name}</li>)}
    </ul>
    </>
  )
}

GraphQLクライアントの候補

こんぶ茶こんぶ茶

urql

graphql-codegenでコード生成できるし、useQueryから返ってくる配列の2つめの値(関数)を使うことで任意のタイミングでクエリを投げられるのでこれでいけるかと思ったが、executeQuery()実行時はまずキャッシュからデータを返して裏でサーバーに問い合わせるようで、おそらくそのせいでSuspenseが効かない(ローディング状態にならない)。

後述するreact-queryの様にキャッシュをクリアする関数が公開されていればなんとかならないこともないが、見た感じなさそうである。

重厚なApolloへのアンチテーゼがテーマらしく軽量で使いやすいしApolloの次に使われてそうだったのもGoodポイントで、できればurqlを採用したいが今のところなんともならない。

index.tsx
...
import {createClient, Provider} from "urql";

const client = createClient({
  url: "http://localhost:8000/v1/graphql",
  suspense: true, // オプションでSuspenseを有効にできる
  requestPolicy: "network-only",
    },
  },
});

ReactDOM.render(
  <Suspense fallback={<h1>Loading...</h1>}>
    <Provider value={client}>
      <UserList />
    </Provider>
  </Suspense>,
  document.getElementById("root")
);
UserList.tsx
import { useQuery } from "urql";
import { GetUserListQuery } from "./generated/graphql";

const UserList = () => {
  const [data, executeQuery] = useQuery(GetUserListQuery, {
    pause: true, // こうしておくとクエリが投げられなくなる
  });

  const handleSearch = () => {
    // useQueryに渡したpauseフラグの状態を無視してクエリを投げられる
    // しかし、Suspenseが効かない...
    // ※executeQuery実行の都度オプションで"network-only"を指定しないとキャッシュから返すだけで終わる
    // createClientで設定したのに...(調べたら、バグではなくそういう仕様だった)
    executeQuery({ requestPolicy: "network-only" });
  }

  return (
    <>
    <button onClick={handleSearch}>検索</button>
    <ul>
      {data.users.map((user) => <li>{user.name}</li>)}
    </ul>
    </>
  )
}
こんぶ茶こんぶ茶

react-query

こちらもurql同様にまずキャッシュから返してバックグラウンドでサーバーに問い合わせる仕様っぽく、Suspenseが効かなかった。
それ以外は要件を満たしていたので惜しい。
swrの影響を受けた?クライアントはどれもStale-While-Revalidate的な動きをするのが辛い。
クライアントを初期化する際にオプションでキャシュを無効にできるようだが変わらず。

ただしこちらはキャッシュをクリアする関数が公開されており、クエリを投げる直前で(検索ボタンのハンドラで)それを使うと、キャッシュヒット無しでいきなりサーバーに問い合わせる&Suspenseも効いて要件を満たすことができた。

しかし、どう見てもライブラリの方向性に沿わない使い方だし、いちいちクエリをキャッシュするのも面倒なのでこれもダメだなあ...と思っていたが、改めて考えるとこれで良いような気もする。

index.tsx
...
import { QueryCache, QueryClient, QueryClientProvider } from "react-query";

// UserList.tsxでこれを使ってキャッシュをクリアする
export const queryCache = new QueryCache();

const queryClient = new QueryClient({
  queryCache: queryCache,
  defaultOptions: {
    queries: {
      staleTime: 1,
      cacheTime: 0, // キャッシュを無効化
      suspense: true,
    },
  },
});

ReactDOM.render(
  <Suspense fallback={<h1>Loading...</h1>}>
    <QueryClientProvider client={queryClient}>
      <UserList />
    </QueryClientProvider>
  </Suspense>,
  document.getElementById("root")
);
UserList.tsx
...
import { useQuery } from "react-query";
import { queryCache } from ".";
// 最終的にはgraphql-codegenでコード生成するがひとまず適当なダミーで検証している
import { fetchUsersByGraphQl } from "xxx";

const UserList = () => {
  const { data, refetch } = useQuery("users", fetchUsersByGraphQl, {
    enabled: false, // こうしておくとクエリが投げられなくなる
  });

  const handleSearch = () => {
    // これでキャッシュがクリアされる
    queryCache.clear();
    // refetch()を使うことで任意のタイミングでクエリ可能
    refetch();
  }

  return (
    <>
    <button onClick={handleSearch}>検索</button>
    <ul>
      {data.users.map((user) => <li>{user.name}</li>)}
    </ul>
    </>
  )
}
こんぶ茶こんぶ茶

graphql-codegenが対応しているクライアントはたくさんあるので、swr系で消耗する前に一度ざっと見てみるのが良さそう。
ただし、Suspenseに対応しているクライアントは少なそうな気がする...。

このスクラップは2021/05/05にクローズされました