useEffectの罠とReact_Queryの威力
16分12秒 | WEBREACTAPIWEBHOOK
基本情報技術者試験の頻出テーマを解説した音声コンテンツです。
トランスクリプト(字幕テキスト)
こんにちは、The Deep Diveへようこそ。あなたが共有してくれた資料をもとに知識の核心に迫っていきましょう。 さて、今回のテーマはReactでのデータ取得です。 Web開発、特にフロントエンドではもう避けて通れない道ですが、正直なところ落とし穴も多いですよね。 今回見ていくのは、あなたが送ってくれたReact Query入門ガイドです。 Free Code CampやLog Rocketといった技術ブログ、それからReact Hooksのベストプラクティスに関するコミュニティの記事、そういった資料一式です。 今回の目的は、このデータ取得という一見すると複雑な仕組みを一緒に分解し、一つ一つの部品がどう動いているのかを理解することです。 そして最終的には、もっと高性能なエンジン、つまりReact Queryのようなライブラリに載せ替えると、どれだけ快適になるのかを体感していくことです。 まずは、伝統的なuseEffectとuseStateを使った手動の方法からじっくり見ていきましょう。 では、基本の部品から見ていきましょう。 私がReactを学び始めた時、APIからデータを取得するならまずuseEffectを使えと教わりました。 どの資料でもここから話が始まっていますし、これは言わばReactにおけるお約束のようなものだと思いますが、 なぜデータ取得はuseEffectなのでしょうか? それと、なぜこれほど多くの開発者がここでつまずくのでしょうか? 良い質問ですね。まずuseEffectの役割は、コンポーネントが画面に描画された後で何か処理を行うことです。 これを副作用、つまりサイドエフェクトと呼びます。 データ取得はまさにその典型例です。コンポーネントの表示自体とは直接関係ない外部との通信ですから。 問題は、この「描画の後で」というタイミングが思った以上に頻繁に発生することです。 なるほど。コンポーネントが再描画されるたびに、エフェクトの中のコードが動いてしまう可能性があるわけですね。 それでよく聞く無限ループに陥ってしまうと。 私も一度APIの無料枠を数分で使い切ってしまった苦い経験がありますよ。 ネットワークタブが真っ赤になって、リクエストが滝のように流れていくのを見た時は本当に血の気が引きました。 それは多くの開発者が通る道だと思います。 その無限ループを防ぐための命綱が、useEffectの第2引数に渡す依存配列(Dependency Array)です。 複数の技術ブログが、これを正しく理解することが第一歩だと強調していますね。 依存配列ですね。 これがどうやって無限ループを止めてくれるのですか? この配列は、Reactに「この中の値が変わった時だけエフェクトを再実行してください」と伝えるための、いわば指示書のようなものです。 もし配列を空、つまり
[]にすると、何にも依存していないため、最初のマウント時に1回だけ実行してくれれば良いという意味になります。 なるほど。 はい。これがページを開いた時に一度だけデータを取得したいという最も一般的なケースで使われるパターンです。 なるほど。指示書ですか。分かりやすいですね。 では、例えば検索機能のようにユーザーが入力したキーワードが変わるたびにデータを再取得したい場合はどうなるのですか? その場合は、その検索キーワードを保持しているステート変数、例えばsearchQueryのようなものを配列の中に入れます。 searchQueryという形ですね。 こうすることでReactはsearchQueryの値が変わるのを監視し、変わった瞬間にだけエフェクト内のデータ取得処理を再実行してくれます。 へえ。 はい、非常にうまくできた仕組みですよね。 いや、本当に。これでいつデータを取得するかは制御できました。 次に、取得したデータをどこに保持しておくかですが、ここでuseStateが登場するわけですね。 資料を読むと、データを保持するステートだけでなく、他にもいくつか用意するのが常識のようです。 その通りです。 Free Code CampやC#コーナーの記事で推奨されているのは、少なくとも3つの状態変数を準備するアプローチです。 まず、取得したデータそのものを入れるためのdata。 次に、データ取得中かどうかを示すブール値のisLoading。 そして最後に、何かエラーが起きた場合にその情報を保持するerrorです。 この3つをセットで管理することで、ユーザー体験が劇的に向上するのです。 isLoadingとerrorですか。 確かに、ただデータが表示されるのを待つのではなく、「読み込み中」と表示されたり、「エラーが発生しました」と教えてくれたりするだけで、ユーザーとしては安心感が全然違いますよね。 空白の画面が一番不安になりますからね。 まさに。UIはアプリケーションとユーザーの対話ですよね。 isLoading状態を使ってスケルトンスクリーンやスピナーを表示したり、error状態を使って具体的なエラーメッセージや再試行ボタンを出したりする。 これら全てが、ユーザーに今何が起きているのかを伝えるための重要なフィードバックになるわけです。 なるほど。 さて、「いつ」「どこに」が定まったので、いよいよ「どうやって」取得するかですね。 ブラウザ標準のFetch APIも使えますが、資料の多くはAxiosというライブラリを推奨しているようです。 これはなぜなのでしょうか? Fetchも素晴らしいAPIですが、Axiosはいくつかかゆいところに手が届く機能を提供してくれます。 Free Code Campの記事がうまくまとめていますが、最大の利点はJSONの扱いです。 Fetchだと、レスポンスを受け取った後に.then(res => res.json())という一手間が必要ですよね。 ああ、ありますね。毎回書きます。 Axiosはこれを自動でやってくれるのです。 レスポンスがresponse.dataの中にすでにJavaScriptオブジェクトとして入っているんですよ。 地味ですが、毎回書く手間が省けるのは嬉しいですね。 はい。それに加えて、エラーハンドリングも直感的です。 Fetchは、サーバーが404や500のようなエラーステータスを返しても、それを成功として扱ってしまうのですが、 Axiosは、そういう場合にちゃんとPromiseをリジェクトしてくれるので、.catchブロックでエラーを捉えやすいのです。 なるほど。 はい。こういう小さな違いが積み重なると、コードの堅牢性に大きく影響してきます。 それでuseEffectの中でAxiosを使うコードを見ていて、ちょっと不思議なパターンに気づきました。 Japanese in Plain Englishの記事などで紹介されていましたが、useEffectの中でわざわざasync関数を定義し、そのすぐ下で呼び出すという書き方です。 最初見た時、「なんでこんな回りくどいことをするんだろう?」と疑問に思いました。useEffect自体をasyncにしてはいけないのですか? それは非常に重要なポイントで、多くの初学者がつまずく罠です。 結論から言うと、useEffectのコールバック関数を直接async関数にすることはできません。 できないのですね。 はい。理由は、async関数は暗黙的にPromiseを返すからです。 ええ、そうですね。 ですが、ReactはuseEffectのコールバック関数の返り値として、クリーンアップ関数というものを期待しているのです。 クリーンアップ関数。 はい。後ほど詳しく話しますが、コンポーネントがアンマウントされる時に実行される、いわばお掃除用の関数のことです。 Promiseとクリーンアップ関数、Reactはどちらなのか判断できずに混乱してしまいます。だからルール違反になるのです。 なるほど。 そこで、useEffectの内部で別のasync関数を定義し、それを呼び出すというパターンが生まれたのです。これならuseEffect自体は何も返さないので、ルールを守りつつ非同期処理が書けるというわけです。 なるほど、そういう理由があったのですね。 すっきりしました。 これでデータの取得と表示、基本的なエラーハンドリングまでの一連の流れが見えてきました。 しかし、資料を読み進めると、現実の世界はもっと手強いと言われている気がしました。 Log Rocketの記事で取り上げられていたメモリーリークの問題とか、 データの読み込み中にユーザーがページを離れてしまったら、一体何が起こるのですか? これは本番環境のアプリケーションでは非常にクリティカルな問題です。 考えてみてください。APIへのリクエストは非同期で行われますよね。 ユーザーがページを離れてコンポーネントが画面から消えた、つまりアンマウントされた後で、APIからのレスポンスが返ってくるケースは頻繁にあります。 はい。 その時、コードは取得したデータでステートを更新しようとするのですが、もう更新すべきコンポーネントは存在しません。 うわ、それはエラーになりますね。「マウントされていないコンポーネントのステートを更新しようとしました」という、あの有名な警告ですか? その通りです。 これがメモリーリークに繋がる可能性もあります。 この問題の解決策が、先ほど少し触れたクリーンアップ関数です。 useEffectは関数を返すことができます。 その返された関数は、コンポーネントがアンマウントされる直前にReactが実行してくれます。 そこで、お掃除をするわけですね。 具体的にはどうやって行うのですか? モダンなJavaScriptには、AbortControllerという素晴らしい仕組みがあります。 データ取得を開始する前にAbortControllerのインスタンスを作成し、そのシグナルをAxiosのリクエストに渡しておきます。 そしてクリーンアップ関数の中でcontroller.abort()を呼び出します。 こうすると、コンポーネントが消える瞬間に、進行中のAPIリクエストに対して「もうそのデータはいらないからキャンセルして」と伝えることができるのです。 なるほど。レストランで料理を注文したけれど、運ばれてくる前に急用ができて、「すみません、やっぱり帰ります」とウェイターに伝える感じですね。 ああ、それすごく分かりやすいですね。 まさにそんな感じです。 厨房は料理を途中で止めてくれるので、無駄なリソースを使わないで済みます。 これにより、不要なステート更新を防ぎ、アプリケーションをより安定させることができます。 これはもうプロの技と言っても良いでしょう。 いやあ、すごいですね。 ただデータを取得して表示するだけなのに、useEffect、useStateの三連隊、async関数の作法、try-catch、そしてAbortControllerを使ったクリーンアップ。 正直、これだけの定型文を全てのAPI呼び出しで書くのはかなり骨が折れますね。 そうなんです。 そしてまさにその痛みを解決するために登場したのが、あなたが共有してくれた資料の主役、React Query入門ガイドで紹介されているReact Query(現在のTanStack Query)です。 待っていました。 このライブラリは一体何者なのでしょうか? なぜこれほどまでに人気があるのですか? 一言で言うなら、私たちが今まで15分近く話してきたことのほぼ全てを、裏側で賢く、そして自動でやってくれるライブラリです。 資料では「サーバーの状態を取得、キャッシュ、同期、更新するためのライブラリ」と定義されています。 ポイントは、これが単なるデータ取得ライブラリではなく、サーバー状態管理ライブラリであるという点です。 サーバー状態管理、どういうことでしょう? コードが具体的にどう変わるのですか? 信じられないほどシンプルになりますよ。 先ほどのたくさんの定型文を含んだ複雑なuseEffectのセットアップが、たった一行に置き換わります。 const { data, error, isLoading } = useQuery('posts', fetchPosts);これだけです。 え、これだけですか?isLoadingやerrorの状態管理もクリーンアップも全部この中に? 全てです。 useQueryフックが、isLoading状態、error状態、そしてもちろんdataそのものを返してくれます。 コンポーネントがアンマウントされれば、リクエストのキャンセルも自動的に処理してくれます。 私たちがただ返ってきた値を使ってUIを描画することに集中すれば良いのです。 まさに魔法のようですね。しかしそれだけではないのですよね。 ガイドでは「キャッシュ」という言葉が何度も出てきました。 これがReact Queryの真骨頂だと。 その通りです。 ここがサーバー状態管理という考え方の核心部分です。 React Queryは、一度取得したデータをメモリ内にキャッシュします。 例えば、ユーザーが投稿一覧ページを見た後、別のページに移動し、また投稿一覧に戻ってきたとします。 その時React Queryは、まずキャッシュにある古いデータを即座に表示します。 ほう。 ユーザーは待ち時間ゼロでコンテンツを見ることができる。 その裏側でライブラリが静かに最新のデータを再取得しに行き、データが更新されていれば画面をスムーズに差し替えてくれるのです。 なるほど。体感速度が劇的に上がるわけですね。 Stale-While-Revalidateという戦略ですね。 ユーザーは常に何かしらのデータをすぐに見ることができ、かつデータは常に最新に保たれる。 これはすごい体験です。 へえ。 これはもはやAPI呼び出しを管理するというレベルの話ではなく、アプリケーション内のサーバー由来のデータを常に新鮮な状態に同期させ続けるという、一段上の抽象度で物事を考えているのです。 データの取得、つまりクエリだけでなく、作成や更新、削除といった操作はどうなるのですか? 例えばフォームを送信するとか。 それにはuseMutationというフックが用意されています。 これも非常に強力で、データの更新処理を簡単にこなせるだけでなく、更新が成功した後に、関連するどのクエリを無効化して再取得させるかを宣言的に指定できるのです。 へえ。 例えば、新しい投稿を追加するミューテーションが成功したら、投稿一覧のクエリを自動的に再取得させるといった連携が簡単に実現できます。 もはや手動でステートを更新する必要はほとんどありません。 なるほど。 React Queryは単なる便利ツールというより、Reactでのデータとの向き合い方を根本から変える一種のパラダイムシフトなのですね。 ただ、こうなってくると逆に心配になることもあります。 これだけ抽象化されてしまうと、開発者はuseEffectの仕組みや非同期処理の難しさなどを全く知らなくてもアプリが作れてしまう。 これって長い目で見ると危険なことではないでしょうか? 素晴らしい視点ですね。まさにその通りだと思います。 React Queryは強力な自動運転システムのようなものです。非常に快適で、ほとんどの場面で私たちを目的地まで安全に運んでくれます。 しかし、予期せぬ事態が起きた時やシステムの挙動をカスタマイズしたいと思った時に、車の仕組み、つまりuseEffectやPromiseといった基礎を理解しているかどうかで、対応できるレベルが全く変わってきます。 マニュアル車で運転を学んでからオートマ車に乗るようなもの、という感じですね。 最高の例えです。 だからこそ今日私たちが辿ってきたように、まず手動でのデータ取得の仕組みをしっかり理解する。 その上でReact Queryのようなツールが、その複雑さをどれだけエレガントに解決してくれるのかを知る。 この両方の知識を持つことが、あなたをより優れた、そして問題解決能力の高い開発者にしてくれるはずです。 いやあ、よく分かりました。 基本を理解することで、ツールのありがたみも、その裏側で何が起きているのかも深く理解できると。 さて、これら全ての情報があなたにとって何を意味するのでしょうか? 私たちはフックを使った手動のデータ取得という基本スキルを解き明かし、同時にReact Queryという、より宣言的で強力な選択肢を見てきました。 最後に、これらの資料から一歩進んで、あなたがじっくり考えるための問いを一つ投げかけたいと思います。 資料ではReact Queryをサーバーの状態を管理するツールとして位置付けていました。 一方で、UIのダークモードのオンオフやモーダルウィンドウの開閉状態といったものは、サーバーとは無関係なクライアントの状態です。 あなたが次に新しい機能を実装する時、こう自問してみてください。 「今私が扱おうとしているこのデータは、データベースに永続化されているサーバーの状態だろうか?」 「それとも、このデバイス、このセッション限りでの一時的なクライアントの状態だろうか?」 この二つを意識的に区別すること。それがあなたのアプリケーションの設計をよりクリーンでより堅牢なものにするための最も重要な第一歩になるかもしれません。このコンテンツは Web society で視聴・学習できます。