本文書はReact Queryのバージョン3を利用して動作確認を行っています。2024年8月の最新バージョンはバージョン5です。名前もReact QueryからTanStack Queryに変わっています。
fukidashi

バージョン5についても記事は下記で確認できます。

Reactを利用してサーバからデータを取得する際に利用する関数やライブラリは何かと聞かれた場合にすぐに思いつくのはfetch関数やaxiosライブラリではないでしょうか。

React QueryはReactのData Fetchingライブラリでサーバからデータを取得する際に利用することができます。サーバからデータを取得するだけならfetch関数とaxiosを利用することで実現できるのでReact Queryを理解するためにはfetch関数とaxiosが持っていない機能が何かを理解する必要があります。

React Queryは内部のデータ取得の処理ではfetch関数やaxiosを利用するためfetch関数やaxiosを置き換えるものではありません。
fukidashi

React Queryの機能について動作確認を行う前にReact Queryをドキュメントを確認しておきましょう。説明では”React Query makes fetching, caching, synchronizing and updating server state in React App a breeze”と記載されており、この一文を読むだけでReact Queryではサーバからのデータ取得だけではなく取得したデータをキャッシュする機能、サーバ上のデータと同期する機能があるライブラリであることがわかります。

言葉ではなんとなくイメージできるかもしれませんが実際に動作確認してみないとどのようなものかわかりません。本文書ではReact環境を構築し実際にReact Queryを使いながらReact Queryの機能の理解を深めていきます。useState Hook, useEffect Hookの使い方を知っているだけのReactのビギナーレベルの人でも理解できようにできるだけ簡単な例を利用して説明しています。

breezeという単語は日本語では微風という意味がありますがここでは簡単という意味で使われています。
fukidashi

Reactのプロジェクトの作成

useQueryの動作確認を行うためにReactのプロジェクトの作成を行います。npx crearte-react-appコマンドを実行します。プロジェクトの名前には任意の名前をつけてください。


 % npx create-react-app react-userquery-test

Reactプロジェクトを作成後、useQueryを利用しない通常の方法でのデータ取得の方法確認し、その後useQueryを利用したデータの取得方法を確認していきます。

useQueryの利用しない場合

useQueryを利用する前にuseQueryを利用しない場合とどこに違いがあるか明確にできるようにuseQueryを利用しない場合で動作確認しておきましょう。

Userコンポーネントの作成

プロジェクトを作成後、srcフォルダにcomponentsフォルダを作成しその下にUser.jsファイルを作成してください。

fetch関数を利用してデータを取得しますが、そのためには外部にデータリソースが必要となります。ここではJSONPlaceholderを利用します。下記のURLにアクセスすると10名分のユーザ情報を取得することができます。ブラウザからも取得が可能なのでブラウザから直接URLにアクセスを行いどのようなデータが取得できるか確認してみてください。


https://jsonplaceholder.typicode.com/users

User.jsファイルではuseEffect Hookを利用してマウント時にfetch関数を実行しJSONPlaceholiderからユーザデータを取得します。取得後はuseState Hookを利用して取得したデータを保存します。その後はmap関数で展開を行いユーザ一覧をブラウザ上に表示させています。


import { useState, useEffect } from 'react';

const fetchUsers = async () => {
  const res = await fetch('https://jsonplaceholder.typicode.com/users');
  return res.json();
};

function User() {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    fetchUsers().then((data) => {
      setUsers(data);
    });
  }, []);

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

export default User;

App.jsファイルの更新

Userコンポーネントの作成が完了したらApp.jsファイルを更新してUserコンポーネントをimportします。


import User from './components/User';

function App() {
  return (
    <div style={{ margin: '2em' }}>
      <h1>ユーザ情報</h1>
      <User />
    </div>
  );
}

export default App;

コンソールでnpm startまたはyarn startコマンドを実行するとブラウザ上には10名分のユーザ一覧が表示されます。

ユーザ一覧表示
ユーザ一覧表示

useQueryを利用しない方法でユーザ一覧を表示することができたので次はuseQueryを利用してユーザ一覧を表示させます。

useQueryを利用する場合

useQueryを利用しない場合の動作確認ができたので次は実際にReact Queryをインストールして動作確認を行います。

useQueryを利用するためにはreact-queryパッケージをインストールする必要があります。


$ npm install react-query

インストールしたreact-queryのバージョンはpackage.jsonファイルから確認することができます。バージョンを確認すると利用しているバージョン3.39.2であることがわかりました。

UserコンポーネントではuseQueryを使ってコードで書き換えます。一時的にmap関数の処理は実行しないようにしておきます。この時点で先ほど利用していたReact HookのuseStateとuseEffectを削除しています。


import { useQuery } from 'react-query';

const fetchUsers = async () => {
  const res = await fetch('https://jsonplaceholder.typicode.com/users');
  return res.json();
};

function User() {
  const result = useQuery('users', fetchUsers);
  console.log(result)
  return (
    <div>
      <h2>ユーザ一覧</h2>
      <div>
        {/* data.map((user) => (
          <div key={user.id}>{user.name}</div>
        ))*/}
      </div>
    </div>
  );
}

export default User;

useQueryではコードにある通り、第一引数に任意の名前のユニークなキー、第二引数にはpromiseを戻す関数を設定します。


const result = useQuery('ユニークキー', Promiseを返す関数)

この状態でデベロッパーツールのコンソールを確認しておきましょう。コンソールにuseQueryを利用で取得したresultの内容が表示されると思ったかもしれませんが画面は真っ白でコンソールには以下のメッセージが表示されます。


Uncaught Error: No QueryClient set, use QueryClientProvider to set one

メッセージを見る限り、QueryClientとQueryClientProviderの設定が必要であることがわかります。

QueryClientの設定

useQueryの動作確認を行う前にUserコンポーネントの親コンポーネントのAppコンポーネントのApp.jsファイルでQueryClientとQueryClientProviderの設定を行う必要があります。

QueryClientProviderで要素を包みQueryClientをインスタンス化したqueryclientを設定します。QueryClientはキャッシュ情報のやりとりに利用されます。


import { QueryClient, QueryClientProvider } from 'react-query';
import User from './components/User';

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <div style={{ margin: '2em' }}>
        <h1>ユーザ情報</h1>
        <User />
      </div>
    </QueryClientProvider>
  );
}

export default App;

useQueryの実行結果の確認

useQueryの実行できる環境が整ったのでブラウザでアクセスしてデベロッパーツールのコンソールを確認してください。useQueryの結果はオブジェクトとして取得でき下記のようなメッセージが表示されます。

useQueryの実行結果
useQueryの実行結果
React 18を利用している場合には同じメッセージが2回表示されます。
fukidashi

結果が入ったresultオブジェクトの中にはユーザ情報だけではなくstatus, isLoadingなどさまざまなプロパティが含まれていることがわかります。

ブラウザのデベロッパーツールのコンソールに表示される2つのオブジェクトのプロパティを比較するとstatus, isLoading, isSuccessなど値が異なっていることがわかります。

statusがloadingのオブジェクトを開いてみます。オブジェクトは複数のプロパティから構成されていることがわかります。

statusがloadingの中身を確認
statusがloadingの中身を確認

statusがsuccessのオブジェクトを開いてみます。

statusがsuccessの中身を確認
statusがsuccessの中身を確認

2つの内容を確認するとstatusがloadingの時はdataプロパティはundefinedです。statusがsuccessの時はdataプロパティにはデータが含まれています。

ユーザの一覧を表示させるためresultの中からdataのみを取得してmap関数で展開させてみましょう。


import { useQuery } from 'react-query';

const fetchUsers = async () => {
  const res = await fetch('https://jsonplaceholder.typicode.com/users');
  return res.json();
};

function User() {
  const { data } = useQuery('users', fetchUsers);

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

export default User;

ブラウザで確認すると”TypeError: Cannot read property ‘map’ of undefined”のエラー画面が表示されます。

先ほどstatusがloadingの場合のdataプロパティがundefinedになっていたことが原因です。loadingの状態ではまだユーザ情報が取得できたいないためdataプロパティに値が入っていません。dataプロパティの値がundefinedなのでmap関数で展開することができないためにエラーが発生しています。

useQueryではstatus, isLoading, isError, isSuccessなどデータだけではなくデータ取得に関するさまざまなプロパティが含まれています。dataプロパティがundefinedによる問題もisLoadingを利用することで解決することができます。データがloading(まだdataプロパティがundefined)の間はisLoadingプロパティの値はtrueになります。データが取得後にisLoadingがfalseになるため下記のようにif文による分岐を利用することでisLoadingがtrueの場合はmap関数が実行されることはありません。


import { useQuery } from 'react-query';

const fetchUsers = async () => {
  const res = await fetch('https://jsonplaceholder.typicode.com/users');
  return res.json();
};

function User() {

  const { data,isLoading } = useQuery('users', fetchUsers);

  if (isLoading) {
    return <span>Loading...</span>;
  }

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

export default User;

isLoadingがfalseからtrueになるまでmap関数を実行されないのでエラーは発生せずユーザの一覧を表示することができます。

useQueryによるユーザ一覧を表示
useQueryによるユーザ一覧を表示

データを取得している間は画面上には”loading…”の文字が表示されます。

loadingを利用せずオプショナルチェーン演算子(?)を利用することでもユーザ一覧は表示されます。


<div>
  <h2>ユーザ一覧</h2>
  <div>
    {data?.map((user) => (
      <div key={user.id}>{user.name}</div>
    ))}
  </div>
</div>

ここまでの設定でuseQueryを利用した方法でもユーザ一覧を表示させることができました。

エラーに対する処理

エラーによりデータが取得できなかった場合の処理についてはisErrorプロパティを利用することで対応することができます。

エラーを発生させるためにfetchを行う先が存在しないURL(https://jsonplaceholder.typicode.co)に変更しています。今回はresultの中からdata, isLoadingに加えてisErrorとerrorを利用します。errorにはエラーの情報が含まれています。


import { useQuery } from 'react-query';

const fetchUsers = async () => {
  const res = await fetch('https://jsonplaceholder.typicode.co');
  return res.json();
};

function User() {

  const { data, isLoading, isError, error } = useQuery('users', fetchUsers);

  if (isLoading) {
    return <span>Loading...</span>;
  }

  if (isError) {
    return <span>Error: {error.message}</span>;
  }

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

export default User;

ブラウザで確認するとすぐにエラーが発生するわけではなくしばらくの間Loadingの画面が表示されます。

Loadingメッセージが表示
Loadingメッセージが表示

一定時間が経過すると画面にはエラーメッセージが表示されます。

エラーメッセージが表示
エラーメッセージが表示

useQueryを利用することで数行のコードだけでエラーへの対応も行えるようになりました。

エラーの処理については理解できましたがLoadingからエラーメッセージ表示までに一体何が裏側で起こっているのか気になるのでさらに調査を進めます。

エラー時にデベロッパーツールを開いてメッセージを見るとエラーが発生しても複数回GETメソッドが実行されていることがわかります。

コンソールメッセージの確認
コンソールメッセージの確認

ネットワークタブを確認すると3回fetchが行われていることがわかります。

ネットワークタブの確認
ネットワークタブの確認

React Queryのドキュメントを確認するとデータが取得できない場合にデフォルトでは3回のリトライを行うことがわかります。React Queryには自動でリトライ機能が設定されています。またリトライの回数はオプションによって変更することもできます。

リトライのデフォルト値の確認
リトライのデフォルト値の確認

動作確認のためにリトライの回数をデフォルトの3から5に変更して動作確認を行います。オプションはuseQueryの第三引数にオブジェクトとして設定することができます。


const { data, isLoading, isError, error } = useQuery('users', fetchUsers, {
  retry: 5,
});

5に変更することでリトライの回数が増えていることが確認できます。

リトライの回数の変更
リトライの回数の変更

メッセージが表示される間隔をみていると回数を重ねるたびに長くなっていることにも気づきます。リトライの間隔についてもドキュメントに記載せれておりデフォルトでは1000msから始まり2倍、さらに2倍と指数関数的に間隔が広くなり最大は30秒に設定されることがわかります。

リトライの間隔
リトライの間隔

リトライを行いたくない場合はretryをfalseに設定してください。即座にエラーメッセージが表示されます。

ウィンドウフォーカスリフェッチ

エラーの動作確認を行っている際に画面に”Error:Failed to fetch”と表示後に再度ブラウザのウィンドウをクリックすると再度Loading画面が表示されデータの取得処理が再実行されます。

これはuseQueryの持つWindow Focus Refetching機能で画面をクリックすると自動でfetchが再実行されます。これはエラーが発生した時に行われるものではなくエラーが発生しない場合でも再フェッチが行われています。Window Focus Refetching機能についても確認していきましょう。

再度正しいUserコンポーネントのURLを正しいURLに戻します。


const res = await fetch('https://jsonplaceholder.typicode.com/users');

ブラウザでアクセスするとユーザ一覧が表示されます。

デベロッパーツールのネットワークタブを開いて一度履歴の削除を行なってください。下記の画面の赤丸で囲まれた禁止マークをクリックしてください。

履歴の削除
履歴の削除

その後画面をクリックするとリフェッチ(データの再取得)が自動で行われていることが確認できます。

リフェッチの確認
リフェッチの確認

画面をクリックすればリフェッチが行われるわけではなくそのウィンドウから一度外れなければ画面を何度クリックしてもリフェッチは行われません。一度そのウィンドウから外れ外側をクリックして再度ウィンドウに戻りクリックを行うとリフェッチが行われます。

Window Focus Refetching機能はデフォルトではonになっているのでオプションでoffにすることもできます。


const { data, isLoading, isError, error } = useQuery('users', fetchUsers, {
  refetchOnWindowFocus: false,
});

設定完了後にウィンドウをクリックしてもリフェッチは行われません。

Window Focus Refetching機能によってサーバ上の最新の情報をユーザが意識することなく自動で取得することができることがわかりました。

キャッシュの動作確認

useQueryの機能の中で重要な機能の一つであるキャッシュについて確認していきます。

サーバからデータを取得するとデフォルトでは5分間(1000*60*5)はキャッシュに保存されます。そのキャッシュ中であれば一度アンマウントしたコンポーネントを再度マウントするとキャッシュからデータを取得するのですぐにブラウザ上にデータを描写することができます。

キャッシュの動作確認を行うためにApp.jsファイルにshowプロパティを設定しshowをtrue,falseと切り替えることでUserコンポーネントの表示、非表示(マウント、アンマウント)を切り替えます。


import { useState } from 'react';
import { QueryClient, QueryClientProvider } from 'react-query';
import User from './components/User';

const queryClient = new QueryClient();

function App() {
  const [show, setShow] = useState(true);
  return (
    <QueryClientProvider client={queryClient}>
      <div style={{ margin: '2em' }}>
        <div>
          <button onClick={() => setShow(!show)}>Toggle</button>
        </div>
        <h1>ユーザ情報</h1>
        {show && <User />}
      </div>
    </QueryClientProvider>
  );
}

export default App;

設定後、ブラウザ確認するとtoggleボタンが表示されます。

表示・非表示を切り替えるためのtoggleボタン
表示・非表示を切り替えるためのtoggleボタン

toggleボタンをクリックすることで表示・非表示が行われることを確認してください。statusを見ることでキャッシュからデータを取得しているかどうか確認を行います。キャッシュを利用していない場合はloadingステータスからsuccessステータスに変わります。キャッシュからデータを取得している場合はloadingステータスはなくsuccessステータスが表示されます。


const { data, isLoading, isError, error, status } = useQuery(
  'users',
  fetchUsers,
);
console.log(status);

アクセスした直後ではキャッシュは存在しないので下記のようにloading, successが表示されます。

ページを開いた直後
ページを開いた直後

Toggleボタンを一度クリックして非表示にした後再度Toggleボタンをクリックしてください。キャッシュからデータを取得しているのでsuccessが表示されます。

キャッシュからデータを取得
キャッシュからデータを取得きゃ

キャッシュから取得していてもフェッチは行われなくなるわけではなくバックグランドで実行されデータの取得は行われています。もしサーバ側でデータの更新が行われている場合はまずキャッシュのデータが表示され、サーバからデータの取得後後に変更したデータが反映されます。

デフォルトではキャッシュ期間は5分なので1秒に変更します。設定はcacheTimeで設定を行うことができます。単位はmsです。デフォルトでは1000*60*5に設定されています。


const { data, isLoading, isError, error, status } = useQuery(
  'users',
  fetchUsers,
  {
    cacheTime: 1000,
  }
);
console.log(status);

ブラウザをリロードして、少し時間を置いてからtoggleボタンを押してください。先ほどとは異なりほんの一瞬なので見えないかもしれませんがloading…が表示されます。statusもsuccessのみではなくloadingの後にsuccessが表示されます。

cacheTimeを1秒に設定した場合
cacheTimeを1秒に設定した場合

もしLoaing…をしっかりみたい場合はネットワークタブでSlow 3Gに変更をして動作を確認してください。

3Gネットワークを設定
3Gネットワークを設定

useQueryでcacheTimeの設定の間であればキャッシュに保存されているデータが利用されることがわかりました。

ここまでの動作確認でコンポーネントがマウントした時、ウィンドウがクリックされた時にリフェッチが行われることを確認しました。そのほかにオプションのrefetchIntervalを設定することで設定した間隔でリフェッチを行うことができます。


  const { data, isLoading, isError, error, status } = useQuery(
    'users',
    fetchUsers,
    {
      refetchInterval: 1000,
    }
  );

バックグランドでは1秒後にフェッチが行われることが確認できます。

Devtoolsの設定方法

React Query専用のdevtoolsも準備されているので利用方法を確認します。devtoolsを利用することでuseQueryで設定したユニークキーごとのクエリーに関する情報を見ることができます。

App.jsファイルを開いてReactQueryDevtoolsをimportします。devtoolsを利用するために追加のパッケージ等のインストールは必要ありません。propsのinitailIsOpenではdevtoolをアクセス時にオープンした状態にするかどうか設定を行うことができます。ここではfalseにしています。


import { useState } from 'react';
import { QueryClient, QueryClientProvider } from 'react-query';
import User from './components/User';
import { ReactQueryDevtools } from 'react-query/devtools';

const queryClient = new QueryClient();

function App() {
  const [show, setShow] = useState(true);
  return (
    <QueryClientProvider client={queryClient}>
      <div style={{ margin: '2em' }}>
        <div>
          <button onClick={() => setShow(!show)}>Toggle</button>
        </div>
        <h1>ユーザ情報</h1>
        {show && <User />}
      </div>
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}

export default App;

設定後ブラウザで確認すると左下にReact Queryのロゴが表示されていることがで確認できます。表示する場所もpropsで設定することができます。

devtools設定後の画面
devtools設定後の画面

クリックするとDevtoolが表示されます。左側のパネルには[“users”]が確認できます。これはuseQueryの第一引数に設定したキーの名前でキーを変更するとここの名前も変わります。このキーによって取得したデータが右側のパネルに表示されます(Data Explorer)。

devtoolsが開いた状態
devtoolsが開いた状態

Data Explorerをスクロールすると設定されているcacheTimeなどのパラメータの値も確認することができます。

Queryのパラメータの確認
Queryのパラメータの確認

上部には現在のステータスが表示され、左からfresh、fetching, stale, inactiveと表示されています。ブラウザでアクセスするとfresh→fetching→staleになります。staleはデータ取得から時間が経過し最新の情報ではないということを表しています。デフォルトではstaleの値は0なのでデータを取得するとすぐにstaleのステータスに移動します。staleのデフォルト値を変更することも可能で変更し時間を伸ばすと設定した値の時間だけfreshのステータスで表示されます。freshはデータが最新情報であることを示します。

Toggleボタンを押すとUserコンポーネントは非表示になるので下記のようにinactiveになります。

非表示にするとinactive
非表示にするとinactive

再度Toggleを押すとstaleの状態になります。

cacheTimeを5分から5秒に変更してToggleボタンを押してください。5秒後にキャッシュが使えなくなると同時にdevtoolから情報が消えます。

このことからdevtoolがキャッシュの情報を見ていることもわかります。devtoolを見ることでuseQueryの状態を目で見て確認することができます。

casheTime5秒を設定し5秒経過した状態
casheTime5秒を設定し5秒経過した状態

useQueryのドキュメントに記述された通り簡単にデータを取得、キャッシュ、サーバデータとの同期が行えることが確認できました。useQueryには本文書で記載したこと以外にもまだまだ機能を持ち、実際に本番で利用するためには理解を深めることが必要ですが、useQueryの基本部分は理解できたのではないでしょうか。