useEffectの中でfetch (取得系のリクエスト)しないでください。以上です。ご清聴ありがとうございました。いいねと高評価、チャンネル登録よろしくお願いします。
おまけ
とはいえ、useEffectの中でデータ取得することを考えなければいけない場合もあります。例えば、React 16をまだ使っている場合とか。React 18以降ならSuspenseがあるので考えなくていいです。
ということで、React 16の世界でどうしてもuseEffectの中でfetchしなければならない場合を最近経験し、その場合にもできる限りベストプラクティスに従いたいということで、考えたことを紹介します。
まだReact 16系に囚われている方は参考にしてください。また、新しいReactを使っている方はこの記事で紹介することをそのまま実践する必要はありませんが、useEffectのベストプラクティスの考え方について学ぶものがあるかもしれません。ぜひおまけも見ていってください。
ベストプラクティス
ということで、これがuseEffectの中でfetchしなければならない場合のベストプラクティスです。
import React, { useEffect, useState } from "react";
type FetchState<T> = {
state: "loading";
} | {
state: "fulfilled";
data: T;
} | {
state: "rejected";
error: unknown;
}
const App: React.FC = () => {
const [fetchState, setFetchState] = useState<FetchState<number>>({ state: "loading" });
useEffect(() => {
if (fetchState.state !== "loading") return;
const controller = new AbortController();
fetch("https://example.com/get-number", {
signal: controller.signal,
})
.then((res) => {
if (!res.ok) throw new Error("エラー(今回はエラー処理は適当)");
return res.json();
})
.then((data) => {
setFetchState({
state: "fulfilled",
data: data.number,
});
})
.catch((error) => {
setFetchState({
state: "rejected",
error,
})
});
return () => {
controller.abort();
};
}, [fetchState]);
// ...
};
ポイント
特にこの記事でベストプラクティスとして強調したいのは、ステートがローディング中を示している場合にfetchすることです。今回の場合、fetchState.state
が"loading"
であることをその条件としており、そうでない場合は早期returnしてfetchしないようにしています。
では、これがなぜベストプラクティスなのでしょうか。それは、useEffectの“正しい使い方”に沿っているからです。
復習すると、useEffectの正しい使い方とは、「コンポーネントが表示されていることの追加の作用」を表現することです。基本的に、Reactにおけるコンポーネントが表示されていることの主たる作用とは、そのコンポーネントの内容がDOMに表示されることです。これがコンポーネントの基本的な用途であるため、この類の作用に対しては返り値でJSX値を返すというAPIが割り当てられていると考えられます。しかし、そのコンポーネントが存在することにより、それ以外の追加の作用が発生することもあります。そのような追加の影響を実装するために使うのがuseEffectなのです。
今回の例の場合、このコンポーネントは「fetchを発生させるという作用」を持っていると解釈できます1。ただ、無条件でfetchを発生させるのではありません。コンポーネントが「ローディング中」という状態にあるときだけfetchが発生するのです。
考え方の逆転
今回ベストプラクティスとして紹介したこのやり方は、useEffect + fetchで良く行われる以下の(ベストではない、あまり良くない)やり方に比べると、考え方が逆転しています。
useEffect(() => {
setFetchState({ state: "loading" });
const controller = new AbortController();
fetch("https://example.com/get-number", {
signal: controller.signal,
})
.then((res) => {
if (!res.ok) throw new Error("エラー(今回はエラー処理は適当)");
return res.json();
})
.then((data) => {
setFetchState({
state: "fulfilled",
data: data.number,
});
})
.catch((error) => {
setFetchState({
state: "rejected",
error,
})
});
return () => {
controller.abort();
};
}, []);
良くないやり方の特によくない点は、依存配列が[]
とされている点です。このように依存配列が[]
のuseEffectは「コンポーネントがマウントされたときに1回だけ実行する」という意味で使われがちですが、これは、前述の「useEffectは追加の作用を記述するために使う」という考え方から離れてしまっているので、バッドプラクティスです。
考え方の逆転というのは、loading
ステートの扱いのことです。「良くないやり方」ではfetchの開始時にステートをloading
にしており、loading
という状態は、fetchの進行状況を反映するのが役割になっています。
一方で、この記事でいうベストプラクティスは、fetchという挙動を「ステートがloading
である場合に発生する、コンポーネントの追加の作用」として捉える考え方に基づいたものです。つまり、fetchの進行に起因してステートがloading
になるのではなく、ステートがloading
であることに起因してfetchが進行するという捉え方になっており、良くないやり方と比べると主従が逆転していることが分かります。
このように考え方を逆転させることで、コンポーネントがマウントされたらfetchが走るということを、「初期ステートがloading
である」というReact的なロジックで表現できるようになり、useEffectの依存配列の濫用が必要なくなります。
ついでに、再読み込みをかけたい場合はステートをloading
状態に戻せばよくなるのも良い点だと考えています。「〇〇したら再読み込みする」という手続き型的になってしまいそうなロジックを、「〇〇したらステートをloading
にする」と言い換えて、ステート変化の世界で完結させることができるからです。
注意: これはあくまでReact 18以降のSuspenseが使えない場合の次善策だということを忘れないでください。ここでベストプラクティスとして紹介したものも、React 18以降のやり方に比べたら良くありません。「ステートをloadingにしたら結果的にデータがロードされる」というのも些かピタゴラスイッチすぎますからね。
まとめ
この記事のおまけでは、どうしてもuseEffectの中でfetchしなければいけない場合にその制約の中で最大限頑張る方法を紹介しました。その場合でも、考え方を逆転させることで、useEffectを「マウント時に実行する」ために使うような誤用を避けて実装することができます。
React 18以降を使っている方はおまけで紹介したテクニックを直接使う必要はありませんが、useEffectをうまく使うための参考になれば幸いです。
-
ただし、この解釈はあくまでReact 16の世界を前提としたもので、React 19など最新の世界観においてはデータ取得はまた異なる考え方が適用されるのでご注意ください。 ↩