useEffectとカオスなAPIコール用コードとの格闘
Reactを学び始めた頃、私はuseEffectとuseStateさえあれば、あらゆるAPIリクエストに対応できると思っていました。しかし、現実はもっと過酷でした。データを取得するたびに、少なくともdata、isLoading、errorという3つのステートを管理しなければならず、プロジェクトが大きくなるにつれてコードは退屈な繰り返しの連続となり、制御が非常に困難になりました。
例えば、ECサイトを開発していると想像してみてください。ユーザーが商品の詳細を見て、一覧に戻り、再び同じ商品をクリックしたとします。純粋なuseEffectを使っている場合、アプリは同じコンテンツに対して2回もAPIを叩きに行きます。これはサーバーのリソースを浪費するだけでなく、ユーザーに不必要なローディングスピナーを見せ続けることになります。
なぜ従来の手法ではプロジェクトがすぐに「崩壊」してしまうのか?
何時間もデバッグを繰り返した結果、最大の過ちはクライアント状態(Client State)とサーバー状態(Server State)を混同していたことだと気づきました。
- Client State: メニューの開閉状態やライト/ダークテーマのような一時的なデータです。これらはフロントエンドのコード内で完全にコントロールできます。
- Server State: サーバー上にあるデータ(記事一覧、ユーザー情報など)です。非同期であり、他者によっていつでも変更される可能性があり、何より重要なのは、アプリがそれを完全に「所有」しているわけではないということです。
useEffectを使うということは、不安定なもの(サーバー状態)を手動で管理しようとしていることを意味します。私たちには、データがいつ古くなった(stale)かを判断し、ユーザーがブラウザのタブに戻ってきたときに自動でデータを再取得できるような、賢いキャッシュメカニズムが欠けていたのです。
本当に投資価値のある解決策とは?
この課題を解決するために、コミュニティでは主に3つのアプローチが検討されます。
- カスタムフックを自作する: コードの重複は減らせますが、キャッシュのロジックやコンポーネント間でのデータ同期に頭を悩ませることに変わりはありません。
- Redux Toolkit Query (RTK Query) を使う: すでにReduxを採用しているプロジェクトなら非常に強力なツールです。しかし、API管理だけが目的ならば、その設定はあまりにも冗長で複雑です。
- TanStack Query (React Query): 最も実用的で洗練された選択肢です。データ取得のロジックをUIから完全に切り離し、自動キャッシュをサポートしており、導入も非常に簡単です。
仕事の過程で、何百行ものJSONを返すAPIに遭遇したときは、TypeScriptのインターフェースを定義する前に toolcraft.app/ja/tools/developer/json-formatter を使ってデータを整形しています。これにより、未整形のテキストを読むよりも大幅に時間を節約できます。
TanStack Queryの実装:基礎からプロフェッショナルまで
1. QueryClientの設定
最初のステップとして、アプリケーションをQueryClientProviderでラップする必要があります。これは、システム全体のキャッシュデータを一括管理する「ハブ」のような役割を果たします。
npm install @tanstack/react-query
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<MyComponent />
</QueryClientProvider>
);
}
2. useQueryによるスムーズなデータ取得
useEffectで何十行もコードを書く代わりに、useQueryフックを使えば数行で済みます。その秘密はqueryKeyにあります。これは、ライブラリがキャッシュ内のデータを識別するためのユニークな名前です。
const { data, isLoading, isError } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
staleTime: 1000 * 60 * 5, // 5分間はデータが「最新」とみなされる
});
ちょっとしたコツ: staleTimeを活用しましょう。商品カテゴリのように変化の少ないデータは、サーバーの負荷を軽減するためにこの時間を長めに設定するのがおすすめです。
3. 楽観的更新(Optimistic Updates):アプリを爆速にする秘策
これは最も価値のある機能です。通常、ユーザーが「いいね」を押すと、APIのレスポンスを待ってからボタンの色が変わりますが、これでは操作感が損なわれます。
楽観的更新を使用すると、操作が成功したかのように即座にUIを更新できます。万が一サーバーでエラーが発生した場合は、システムが自動的に以前の状態にロールバックします。これにより、ユーザーはアプリが瞬時に反応しているように感じます。
const mutation = useMutation({
mutationFn: updatePost,
onMutate: async (newPost) => {
// コンフリクトを避けるため、他のクエリをキャンセルする
await queryClient.cancelQueries({ queryKey: ['posts'] });
// エラー発生時に備えて現在の状態を保存する
const previousPosts = queryClient.getQueryData(['posts']);
// UIを即座に更新する(APIが完了する前に実行)
queryClient.setQueryData(['posts'], (old) =>
old.map(post => post.id === newPost.id ? { ...post, ...newPost } : post)
);
return { previousPosts };
},
onError: (err, newPost, context) => {
// エラーが発生した場合は、すぐに古いデータに戻す
queryClient.setQueryData(['posts'], context.previousPosts);
},
onSettled: () => {
// 完了後は常にサーバーと同期させる
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
});
覚えておくべき実戦の教訓
多くの実際のプロジェクトにTanStack Queryを導入した結果、私は3つの黄金律を導き出しました。
- 階層的なQueryKey構造: キャッシュの管理や削除を容易にするために、
['user', userId]や['products', 'search', query]のようにルール化されたキーを設定してください。 - ユーザー体験を優先する: 画面を覆い隠すようなローディングスピナーを多用するのは避けましょう。キャッシュ内の古いデータを表示しつつ、上部に小さなローディングバーを出すだけで、非常にスムーズな印象を与えられます。
- Devtoolsを必ず導入する: このツールを使えば、各クエリの動作状況やデータの鮮度を一目で把握でき、デバッグが劇的に楽になります。
結論として、単なるコーダーからプロフェッショナルなフロントエンドエンジニアにステップアップしたいのであれば、TanStack Queryの習得は不可欠です。コードをクリーンに保つだけでなく、従来のuseEffectでは到達できない次元のユーザー体験を提供できるようになります。

