React開発におけるサーバーステートマネジメント

media thumbnail

こんにちは、株式会社Gizumoでエンジニアをしている田中です。

現在React(Next.js)で開発している自社サービスで「TanstackQuery」ライブラリの採用が決まりました。
社内で使用経験のないライブラリであることから社内の共通認識を持つ目的を兼ねて、TanstackQueryとは何か、どのように扱うのかを見ていきます。

前提・対象とする読者

この記事は以下を前提として読み進めてください。

  • サンプルコードはReactTypeScriptで記述
  • Reactが提供する機能について等の解説は最小限で止める

また、対象とする読者は以下のような方を想定しています。

  • ReactJavaScriptで可)や他のフロントエンドフレームワーク(以下FW)の使用経験がある方
  • FWで用いられる「状態(state)」についてなんとなく理解がある方
  • Reactの状態管理設計について検討中の方

TanstackQueryとは

TanstackQuery(旧ReactQuery)とはフロントエンド開発で用いられる「データフェッチングに伴う処理をサポート」するためのライブラリです。あくまでサポートの立ち位置であり、fetch関数やAxiosなどと併用することを前提とした設計になっています。
サポートの主な目的は「状態管理」です。
状態管理の中でも「サーバーから取得したデータの状態管理」をメインに扱います。

TanstackQueryの特徴

TanstackQueryの特徴は大きく分けて2つあります。

1) 宣言的記述

TanstackQueryを用いれば宣言的な記述によりコードの見通しがとても良くなります。
具体例として初期表示でデータを取得する処理をTanstackQueryを「使用する例」と「使用しない例」で記述してみます。
前提としてデータ、ローディング、エラーの状態が伴うものとします。

TanstackQueryを使用しない例】

import { fetchTodos } from '@/my-api';
import { Todo, MyErrorType } from '@/types';

const MyComponent: FC = () => {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<MyErrorType>(undefined);

  const _initAsync = async () => {
    setIsLoading(true);
    try {
      const fetchData = await fetchTodos();
      setTodos(fetchData);
    } catch (error) {
      setError(error);
    } finally {
      setIsLoading(false);
    }
  };

  useEffect(() => {
    _initAsync();
  }, []);

  if (isLoading) return <div>Loading...</div>
  if (error) return <div>{error.message}</div>

  return (
    <ul>
      {data.map((todo) => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  )
}

TanstackQueryを使用する例】

import { useQuery } from '@tanstack/react-query';
import { fetchTodos } from '@/my-api';
import { Todo } from '@/types';

const MyComponent: FC = () => {
  const { data, isLoading, error } = useQuery<Todo[]>(['todo'], {
    queryFn: fetchTodos,
  });

  if (isLoading) return <div>Loading...</div>
  if (error) return <div>{error.message}</div>

  return (
    <ul>
      {data.map((todo) => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  )
}

両者を比較してみる

TanstackQueryを使用しない例

では、useEffectを使ってuseStateで定義したstateを更新する手続きを行なっています。

TanstackQueryを使用する例

では、それぞれの状態をuseQueryメソッドから取得しているだけです。また、stateをいっさい定義していません

余計な手続きを捨てよう

公式は下記のように謳っています。

Toss out that granular state management, manual refetching and endless bowls of async-spaghetti code. TanStack Query gives you declarative, always-up-to-date auto-managed queries and mutations that directly improve both your developer and user experiences.

こちらを翻訳すると、

きめ細かな状態管理、手動での再フェッチ、延々と続く非同期スパゲッティ・コードを捨てましょう。TanStack Queryは、宣言的で常に最新の自動管理されたクエリとミューテーションを提供し、開発者とユーザーの両方のエクスペリエンスを直接向上させます。

とのことで、
TanstackQueryでデータフェッチの宣言的記述を行うことでプログラム上の手続きの数が減り、Reactが管理するstateの数を減らすことができます。
宣言的記述の例でもその違いを実感いただけたのではないでしょうか。

2) 状態管理

状態管理はTanstackQuery最大の特徴です。
状態管理をTanstackQueryに任せる上で知っておくべき4項目を解説します。

  • サーバーの状態を管理する
  • ブラウザのキャッシュ機構を利用する
  • 状態へのグローバルアクセスが可能
  • 専用のdevToolsを利用する

サーバーの状態を管理する

状態管理といえばReact HooksuseState, useReducer)やRedux等の状態管理ライブラリが思い浮かびます。
React HooksReduxでは「UIの状態」や「サーバーから取得したデータの状態(※以下より「サーバーの状態」)」を管理できますが、TanstackQueryは「サーバーの状態」にスコープを絞って状態管理を行います。

よって状態管理の分野においてReact HooksReduxとの分業が可能になり、
React HooksReduxは「UIの状態」を管理し、TanstackQueryは「サーバーの状態」を管理してそれをReactのコンポーネントから利用するという構図を作ることができます。

ブラウザのキャッシュを利用する

TanstackQueryはWebブラウザのキャッシュ機構を利用した状態管理を行います。
キャッシュを利用するため、画面を更新すると保存したデータは削除されます。
その面でLocalStorageSessionStorageとは用途が異なります。

Stale while Revalidate

Important Defaults

TanstackQueryのキャッシュを利用した状態管理はStale while Revalidateという考えのもと行われます。

Stale while Revalidateは「キャッシュデータの鮮度を確認し、キャッシュデータが古い場合、一時的にキャッシュデータを返しておいて、裏で通信を行い最新のデータを返す、キャッシュデータが古くなければそのままキャッシュデータを返す」という考え方です。

とてつもなくややこしいですが、この挙動によって「初期表示以外はデータが表示され続けているためUIが大きく変更されない」というUXを実現できます。そのため初期表示以外はローディングUIを必要としません。

TanstackQueryオプションの設定例

「キャッシュデータの鮮度」はTanstackQueryオプションのstaleTimeで設定できます。
デフォルトは0で設定されており、Infinityまで指定可能です。

staleTime:0

キャッシュデータを常にstale(古い)とみなし、画面遷移のたびにリフェッチを行いサーバーから最新のデータを取得します。

staleTime:Infinity

キャッシュデータを常にfresh(新鮮)とみなし、画面遷移を何度行おうとリフェッチを行わずキャッシュデータを返します。画面更新等でキャッシュデータが破棄されるまで、初期表示で取得したデータが常に表示され続けます。

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: Infinity
    },
  },
});

状態へのグローバルアクセスが可能

TanstackQueryで管理する状態はグローバルにアクセスが可能です。
「グローバルに」というのは、階層関係なくどのコンポーネントからでも平等にアクセスが可能ということです。そのためコンポーネント間のPropsを必要としません。

この仕組みを利用することでサーバーの状態管理のためにRedux等の状態管理ライブラリ(store)を用意する必要がありません。

専用のdevToolsを利用する

TanstackQueryには専用のdevToolsが用意されています。Reactであれば下記コマンドでインストールして利用できます。

npm i @tanstack/react-query-devtools

TanstackQueryの状態は一意なキーに紐づいて管理され、それをdevTools上から確認できます。

また、状態のデータ詳細だけでなく、その状態がfresh(新鮮)かstale(古い)かやリフェッチが行われているタイミングなど、TanstackQueryを用いて状態管理する上で必要な情報をまとめて確認できます。

※画像右中央Data ExplorerDataは左上メニューからstale(古い)状態であることがわかります。

TanstackQueryの使い方

TanstackQueryの使い方は大きく2つに分けることができます。
それぞれどのように行うのか簡単に見ていきましょう。

1) 状態の参照と登録 useQuery

状態を参照・登録するには、useQueryを使用します。

const { data, isLoading, error } = useQuery<Todo[]>(['todo'], {
  queryFn: fetchTodos,
});

useQueryの第1引数にはキャッシュに保存してある状態と紐づく「一意なキー」が必要です。
画面の初期表示時、useQueryが一番最初に実行されたタイミングでキャッシュにデータが登録され、指定したキーが登録されたデータと紐づけられます。

useQueryを用いてデータを参照する際はStale while Revalidateが働くため、初期表示以外は通信を待つことなくデータが表示されたUIを維持できます。

2) 状態の更新 useMutation

下記コードは選択したTodoを更新する例です。

キャッシュの状態を更新するにはTanstackQueryからuseMutationというHooksを利用します。
useMutationの第1引数には更新用のAPIが入ります。useMutationを実行することで第1引数のAPIが実行されバックエンドの更新が行われます。

第2引数のonSuccessメソッドはAPIの処理が成功後実行されます。引数にはAPIの実行結果が渡されます(data)。

const updateTodoMutation = useMutation({
  mutationFn: updateTodo,
  onSuccess(data, variables) {
    ...
  },
});

キャッシュデータの上書き

useQueryClientを使用することで現在のキャッシュにアクセスするためのオブジェクトを取得できます。
※サンプルコードでは取得したオブジェクトをclientという変数に格納しています。

client.setQueryDataでキャッシュデータを上書きできます。第1引数には対象の状態と紐づくキーを指定し、第2引数のコールバック関数から返された値がキャッシュにセットされます。

今回は状態の更新を例にとって実装していますが、キャッシュのデータを更新するという意味では作成も削除も変わりません。更新・作成・削除のイベントに合わせて、キャッシュのデータを更新することでキャッシュとサーバーの状態を合わせます

import { useMutation, useQueryClient } from '@tanstack/react-query';
import { updateTodo } from '@/my-api';
import { UpdateTodoDTO } from '@/types'

// 現在のキャッシュデータにアクセスするためのオブジェクト
const client = useQueryClient();

// 定義
const updateTodoMutation = useMutation({
  mutationFn: updateTodo,
  onSuccess(data, variables) {
    client.setQueryData<Todo[]>(['todo'], (prev) => {
      if(!prev) return []
      return prev.map((todo) => {
        if (todo.id === data.id) return data;
        return todo;
      });
    });
  },
});

// 使用
const handleUpdate = ({ id, title, completed }: UpdateTodoDTO) => {
  updateTodoMutation.mutate({ id, title, completed });
};

TanstackQueryの使い方まとめ

状態の参照と登録

  • useQueryを使用します。初期表示に一度呼ぶことでキャッシュへのデータの登録が行われ、Stale while Revalidateによってデータの鮮度に合わせて状態が返されます。

状態の更新

  • useMutationを使用します。setQueryDataを使用することによってキャッシュのデータを更新できます。
  • データの更新をしたい時、新しくデータを作成した時、データを削除した時にサーバーの処理に応じてキャッシュデータを更新する必要があります。

最後に

今回、弊社の自社開発アプリケーションでは以下の理由でTanstackQueryの採用が決まりました。

  • 複雑な状態管理ライブラリの代替となる
  • ReactSuspenseErrorBoundaryと相性が良い
  • モダンフロントエンド開発でスタンダードに採用されている傾向にある

はじめて使用するライブラリなので、知見を溜めつつ、実装する上で見えてくるもの、設計に関することについて今後当ブログで共有できたらなと思います。

ここまで読んでいただきありがとうございました。

参考

少しでも開発にお困りの方は
相談しやすいスペシャリストにお問い合わせください

お問い合わせ
  1. breadcrumb-logo
  2. メディア
  3. React開発におけるサーバーステートマネ...