🗽

【脱Redux】SWRやReact Queryを使った状態管理戦略

2022/09/24に公開

mutexの桝田です!

Reactのデータフェッチに、Vercel社が提供する「SWR」やTanStackコミュニティが提供する「React Query(TanStack Query)」が使われることが多くなってきています。

https://swr.vercel.app/ja

https://tanstack.com/query/v4

これらのライブラリは単なるフェッチだけでなく、キャッシュやデータの更新を担ってくれます。また、Reactが志向する「宣言的」な記述を体現できることも大きなメリットです。

https://zenn.dev/takepepe/articles/fetch-error-convolution

本稿では、(我々が採用する)React Queryにフォーカスし、React Queryを使って実現している状態管理について説明します。SWRを普段お使いの方に関してもかなり共通する部分が多いのではないかと思います。

1. 対象読者

  • Reactのデータフェッチライブラリの使用を検討している方
  • 普段SWRやReact Queryを使用している方
  • 普段Reactを使用するすべての方

2. 状態管理戦略

2.1. 前提としてのディレクトリ構成

状態管理戦略について説明する前に我々が採用するデザインパターンである「Tree Design」について説明します。詳細は以下のページに詳しく書かれています。

https://zenn.dev/tmgauss/articles/beca85dd7fdcae

基本的に src 以下は次のような構成になっています。

src
├── __test__
├── pages
│   ├── _app.tsx
│   ├── _document.tsx
│   └── something.tsx
├── components
│   ├── FirstThing
│   │   ├── index.tsx
│   │   ├── elements
│   │   │   └── SecondThing
│   │   │       ├── index.tsx
│   │   │       ├── elements
│   │   │       ├── hooks
│   │   │       ├── contexts
│   │   │       └── styles
│   │   ├── hooks
│   │   ├── contexts
│   │   └── styles
│   └── shared
│       ├── elements
│       ├── hooks
│       ├── contexts
│       └── styles
├── api
│   └── index.ts
├── styles
│   └── theme.ts
└── modules

「Tree Design」という命名から分かる通り、基本的にはコンポーネントが木構造の形で連鎖しています。src/pages 配下はただのルーティングとして扱い、src/components 配下の最上位のコンポーネント(上の例で言うところのFirstThing)を読み込むだけに使用します。

2.2. 状態管理概観

本稿では、状態を以下のように3つに分類します。

分類 概要 使用例
ページ間ステート ページを跨いで使用される状態。 userに関するデータ、履歴に関するデータ
ページ内ステート ページ内で完結する状態。いわゆるグローバルステート。 フェッチされたデータ、その他様々なデータ
ローカルステート コンポーネント単位の状態。基本的にはuseStateを用いて定義される。 UI要素の状態に関するデータ

我々はReact Queryを分類におけるページ内ステートとして使用します。
つまり、ページごとにContextを作り、それを子孫コンポーネントに渡すことで、各コンポーネントでデータ(とその状態)を参照できるようにします。

2.3. React Queryを用いた状態管理

具体的な方法は以下の通りです。

2.3.1. APIクライアントを定義する

まず、エンドポイントのそれぞれに対してAxiosなどをベースにfetcher / mutate関数を定義します。

2.3.2. PageごとのStateをProviderに定義する

まず、PageごとにContextを生成します。 そのPageで必要なデータに紐づくfetcher / mutate関数を useQueryuseMutation に渡して、Provider内に定義します。Queryのオブジェクトをそのまま子孫へ流します。

実際に使用しているスニペットは以下です。(コメントは本稿のために入れました。)

${camelCase}.tsx
import { createContext, ReactNode, FC } from 'react';

export type ${PascalCase} = {};

export const ${camelCase}Context = createContext<
  ${PascalCase} | undefined
>(undefined);

const { Provider } = ${camelCase}Context;

interface ${PascalCase}ProviderProps {
  children: ReactNode;
}

export const ${PascalCase}Provider: FC<${PascalCase}ProviderProps> = (
  props,
) => {
  const { children } = props;
  
  // Define here

  return (
    <Provider value={{}}>
      {children}
    </Provider>
  );
};
オブジェクトをそのままContextに渡す理由

QueryResultのdata部分だけでなく、QueryResultをそのまま全て投げることで、それぞれの通信の状態を知ることができます。React Queryでは例えば、

const something = useQuery(/* ... */);

と定義されていれば、各子孫コンポーネントsomething.isLoadingsomething.data のように状態にアクセスできます。

2.3.3. hookを定義し、各コンポーネントで使用する

それを使用するためのhookを定義し、各コンポーネントではそれを使って、データを取得します。これにより宣言的にデータ取得ロジックを記述することができます。

実際に使用しているスニペットは以下です。(コメントは本稿のために入れました。)

use${PascalCase}.ts
import { useContext } from 'react';

type Use${PascalCase} = () => ${PascalCase};

export const use${PascalCase}: Use${PascalCase} = () => {
  const context = useContext(${camelCase}Context);

  if (!context) throw new WithoutProviderError(); // Custom Error

  return context;
};

3. どのような恩恵があったか?

実際にこの構成を何度かプロジェクトで使用した結果、大きく分けて2つの恩恵があることを実感しました。

3.1. 状態を正確にUIに反映しやすくなった

我々はデータのI/Oについて

  • データ読み込み中
  • データ読み込み後
    • データの中身あり
    • データの中身なし

の3つの状態に対してUIを定義しています。React Queryのシンタックスに標準化されることで、このそれぞれの状態に対してUIを定義しやすくなりました。

つまり、something.isLoading の場合にはサーキュラーやスケルトンを表示し、そうでない場合には something.data の有無によって内容を書き分けるといったイメージです。
冒頭でも紹介しような宣言的な記述が可能になりました。

我々の整理

詳しく説明すると、我々はフロントエンドのコードを次のように整理しています。

[ UI <=> hooks <=> query client <=> api client ] <===> (server) 

それぞれは次のような意味で使用しています。

  • UI:レンダリングされるコンポーネント(return の中身)
  • hooks:フック(例えば useQuery
  • query client:検証などをして、api clientに必要な情報を渡す(例えば useQuery に渡すフェッチ関数)
  • api client:axiosなどでリクエストを送る(ここはAPI Docsの代わりにバックエンド側に生成させてる)

3.2. データの更新ロジックを見通し良く記述できるようになった

例えば、POSTでデータを作成した後にGETで取得してきたデータをリフレッシュしたいという場面があります。このような際でも、POST後にkeyを指定してデータをinvalidateすれば良く、見通し良くリフレッシュのロジックを記述することが可能になりました。

4. まとめ

ここまでReact Queryを使った状態管理の方法として、「ページごとにuseQueryやuseMutationを定義し、それをContextを使って子孫コンポーネントへ渡す」手法を紹介しました。そうすることで、React Queryによるキャッシュやリフレッシュの恩恵を受けることができるようになり、パフォーマンス向上に繋がります。

非定期にはなりますが、フロントエンド開発に関して継続的な投稿を目指しておりますので、興味のある方はぜひフォローよろしくお願いいたします!
また、mutexの開発に少しでもご興味を持っていただけたら、以下のリンクからお問い合わせいただければ幸いです!

https://www.mutex-inc.dev

mutex Official Tech Blog

Discussion