React Queryはデータフェッチライブラリではない。非同期の状態管理ライブラリだ。 - Qiita
はじめにこの記事はDominikさんが執筆された「Thinking in React Query」を参考にReact Queryの考え方をまとめたものになります。DominikさんはTanStac...
https://qiita.com/taisei-13046/items/05cac3a2b4daeced64aa
Webアプリをつくるにあたって、メインではないけれども重要な要素があります。
それが以下の2つです。
今回はこれらの画面をササッと作る方法を確認します。
「ササッと」とはいえ、今回も全ソースコードを載せているので全体の文量は長めです。
プルダウンで選択したデータを読み込むときにローディング画面を表示させます。
不正なデータを選択された場合にはエラー画面を表示させます。
本記事においては以下を使用しています。
React:18.2.0
TypeScript:5.2.2
Vite:5.2.2
Node.js:18.20.2
React QueryとReact Error Boundaryのインストールを行います。
npm install @tanstack/react-query react-error-boundary
※本記事作成時点でのそれぞれのバージョンは以下の通りです。
@tanstack/react-query:5.51.23
react-error-boundary:4.0.13
React Queryは非同期の状態管理ができるライブラリです。データフェッチをしたときのローディング状態の管理などに向いています。後述のSuspenseとの相性が非常に良いです。
詳細については以下のQiitaの記事が非常に参考になります。
なお、便宜上React Queryと記載していますが、正確にはTanStack Queryと名前が変わっています。
React Error Boundaryは画面描画中のエラーを拾って、壊れたコンポーネントの代わりにエラー表示用のコンポーネントを表示させるためのライブラリです。
詳細については以下の公式サイトの説明が参考になります。
(ライブラリ自体ではなく、エラーバウンダリの概念についての説明になります。)
メインとなるコンポーネントのソースコードは以下の通りです。
/* App.tsx */
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import DataDisplay from "./DataDisplay";
import { useState } from "react";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 1,
},
},
});
const App = () => {
const [id, setId] = useState("1");
return (
<QueryClientProvider client={queryClient}>
<div>
<h1>デモアプリ</h1>
<select value={id} onChange={(e) => setId(e.target.value)}>
<option value="1">アイテム1</option>
<option value="2">アイテム2</option>
<option value="3">アイテム3</option>
<option value="4">無効なアイテム</option>
</select>
<DataDisplay id={id} />
</div>
</QueryClientProvider>
);
};
export default App;
ポイントは1つです。
React Queryでデータを取得するuseQuery系の処理を使用する場合には、事前の設定が必要となります。
QueryClientでインスタンスを生成し、生成したインスタンスをQueryClientProviderに設定します。
これにより、QueryClientProviderコンポーネントで囲んだコンポーネント内でuseQuery系のデータフェッチ処理が使用できるようになります。
データを表示するコンポーネントのソースコードは以下の通りです。
/* DataDisplay.tsx */
import { Suspense } from "react";
import { ErrorBoundary } from "react-error-boundary";
import Loading from "./Loading";
import ErrorFallback from "./ErrorFallback";
import DataContent from "./DataContent";
import { useQueryErrorResetBoundary } from "@tanstack/react-query";
const DataDisplay = ({ id }: { id: string }) => {
const { reset } = useQueryErrorResetBoundary();
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => {
reset();
}}
>
<Suspense fallback={<Loading />}>
<DataContent id={id} />
</Suspense>
</ErrorBoundary>
);
};
export default DataDisplay;
ポイントは2つです。
Reactのコンポーネントでエラーが発生した場合、そのエラーについてのログが画面に出力されるなどして、画面が正しく表示されなくなります。
ErrorBoundaryコンポーネントを使うことで、このコンポーネント内でエラーが発生した場合、FallbackComponentに指定したコンポーネントが代わりに表示されます。
また、エラー状態から復帰するための処理をonResetに指定することができます。
指定した処理は、FallbackComponentに指定したコンポーネント内でpropsとして受け取ることができます。
今回はuseQueryErrorResetBoundaryのresetを設定しました。
直前のクエリのエラー状態をリセットし、クエリを再実行することができる処理となっています。
SuspenseはもともとReactに用意されているコンポーネントです。このコンポーネント内の読み込みが完了するまでは、fallbackに指定したコンポーネントが表示されます。
ローディング画面の実装自体は、このSuspenseを使うことで簡単に実装できます。
エラー表示のコンポーネントのソースコードは以下の通りです。
/* ErrorFallback.tsx */
import { FallbackProps } from "react-error-boundary";
const ErrorFallback = ({ error, resetErrorBoundary }: FallbackProps) => {
return (
<div role="alert">
<p>エラーが発生しました:</p>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>再試行</button>
</div>
);
};
export default ErrorFallback;
ポイントは2つです。
FallbackPropsはReact Error Boundaryで用意している型定義です。
errorにはエラー発生時のエラー情報のオブジェクトが自動で設定されます。
プロパティとしてはmessageのほか、スタックトレースを取得できるstack、エラー名を取得できるnameがあります。
resetErrorBoundaryには、ErrorBoundaryコンポーネントのonResetに設定した処理が渡されます。
今回は再試行ボタンを押すことでエラーリセット処理を実行するようにしました。
ローディング用コンポーネントのソースコードは以下の通りです。
/* Loading.tsx */
const Loading = () => {
return <div>ロード中...</div>;
};
export default Loading;
データ用コンポーネントのソースコードは以下の通りです。
useDataの中身は後ほど確認します。
/* DataContent.tsx */
import { useData } from "./functions";
const DataContent = ({ id }: { id: string }) => {
const { data } = useData(id);
return (
<div>
<h2>{data.title}</h2>
<p>{data.description}</p>
</div>
);
};
export default DataContent;
データ取得処理のコードは以下の通りとなります。
/* functions.ts */
import { useSuspenseQuery } from "@tanstack/react-query";
interface DataItem {
id: string;
title: string;
description: string;
}
const mockData: Record<string, DataItem> = {
"1": {
id: "1",
title: "最初のアイテム",
description: "これは最初のアイテムです",
},
"2": {
id: "2",
title: "2番目のアイテム",
description: "これは2番目のアイテムです",
},
"3": {
id: "3",
title: "3番目のアイテム",
description: "これは3番目のアイテムです",
},
};
export const fetchData = async (id: string): Promise<DataItem> => {
// 擬似的な遅延を追加
await new Promise((resolve) => setTimeout(resolve, 1000));
if (id in mockData) {
return mockData[id];
}
throw new Error("データが見つかりません");
};
export const useData = (id: string) => {
return useSuspenseQuery({
queryKey: ["data", id],
queryFn: () => fetchData(id),
});
};
ポイントは1つです。
データ取得をするための処理です。引数としてオブジェクトを渡すことで、様々な設定をすることができます。
queryKeyはクエリを一意に特定するためのキーです。
queryFnにはデータをフェッチする処理を指定します。
必須のプロパティはこの2つで、他にも様々なプロパティがあります。
useSuspenseQueryはReactのSuspenseといっしょに使うことを想定した作りになっており、ここで指定した処理が完了するまでは、ローディング状態として扱われます。
ローディング、およびエラー画面はメインの処理ではないのでおろそかになりがちですが、ユーザの使いやすさを考えると、それぞれしっかり作り込んでおくことが必要です。
今回紹介した2つのライブラリを使うことで、エラーやデータの取得状態を特別意識することなく、簡単に処理を作成することができました。
Reactには便利なライブラリが他にもたくさんあるので、うまく使ってより実践的なWebアプリを作っていきたいですね。