前回の記事では、useEffect の正しい使い方について整理しました。
今回は反対に、「そもそもuseEffectを使わなくてよいパターン」 を整理します。
React公式ドキュメントでは、こうした「useEffectを使わなくてよいパターン」が明確に整理されています。
本記事では、その内容をもとに、useEffect を書く前に立ち止まるための視点を整理します。
今回の内容と合わせることで、使うべき場面 と 使わなくてよい場面 の両方を把握できるようになります。
2. useEffectを使用しないパターン
2-1. stateから計算できる値を、useEffectで別のstateにしない
最もやりがちなパターンです。
例えば、firstName と lastName の 2 つの state 変数を持つコンポーネントがあるとします。
これらを連結して fullName を計算したいとき、以下のように書きたくなることがあります。
const [firstName, setFirstName] = useState("Taylor");
const [lastName, setLastName] = useState("Swift");
const [fullName, setFullName] = useState("");
useEffect(() => {
setFullName(firstName + " " + lastName);
}, [firstName, lastName]);
一見問題なさそうに見えますが、fullName は firstName と lastName からそのまま求められる値です。
わざわざ state として持つ必要はなく、以下のように直接計算すれば十分です。
const fullName = firstName + " " + lastName;
なぜuseEffectが不要なのか
useEffect の中で setState を行うと、次のような流れになります。
- まず
firstNameまたはlastNameの変更でレンダリングされる - その後
useEffectが実行される -
setFullNameによって再度レンダリングされる
本来その場で計算できる値のために、余分なレンダリングが発生してしまいます。
このパターンで意識すること
既存の state や props から計算できる値は、新たな state として持たず、その場で計算する。
2-2. ユーザー操作に応じた処理をuseEffectに書かない
次に見直したいのは、ユーザー操作をきっかけとした処理 です。
例えば、ボタン押下時に API を実行したい場合に、フラグを state で管理し、
useEffect でその変化を監視して処理を書くことがあります。
const [isBuying, setIsBuying] = useState(false);
useEffect(() => {
if (isBuying) {
buyProduct();
showNotification("購入しました");
}
}, [isBuying]);
この場合は useEffect を使うよりも、ボタン押下時のイベントハンドラに直接書いた方が自然です。
const handleBuy = async () => {
await buyProduct();
showNotification("購入しました");
};
<button onClick={handleBuy}>購入</button>
なぜuseEffectが不要なのか
このケースで行いたいことは、画面が表示されたから実行する処理 ではなく、
ユーザーがボタンを押したから実行する処理 です。
イベントハンドラに書くことで、
- 何がきっかけで処理されるのかが明確になる
- stateや依存配列で遠回しに制御しなくてよい
- 意図しない再実行を防ぎやすい
というメリットがあります。
このパターンで意識すること
ユーザー操作をきっかけとした処理は、まずイベントハンドラに書けないかを考える。
2-3. 表示用データの加工をuseEffectで行わない
画面表示用にデータを加工するためだけに useEffect を使うパターンもよく見かけます。
例えば、受け取った配列データを画面用に整形するために、以下のように書くことがあります。
const [processedItems, setProcessedItems] = useState([]);
useEffect(() => {
setProcessedItems(
rawItems.map(item => ({
id: item.id,
label: item.name.toUpperCase(),
}))
);
}, [rawItems]);
この場合も processedItems は rawItems から計算できる値であり、
useEffect で別 state へ詰め直す必要はありません。
const processedItems = rawItems.map(item => ({
id: item.id,
label: item.name.toUpperCase(),
}));
なぜuseEffectが不要なのか
2-1 と同様に、useEffect 内で setState を行うことで余分なレンダリングが発生します。
- まず
rawItemsの変更でレンダリングされる - その後
useEffectが実行される -
setProcessedItemsによって再度レンダリングされる
rawItems から計算できる値であれば、レンダリング中にそのまま計算する方が効率的です。
重い処理の場合はどうするか
この計算が重く、毎回実行したくない場合は useMemo を使います。
const processedItems = useMemo(() => {
return rawItems.map(item => ({
id: item.id,
label: item.name.toUpperCase(),
}));
}, [rawItems]);
このパターンで意識すること
表示のための加工は副作用ではなく、計算として扱う。
2-4. state更新のuseEffectを数珠繋ぎにしない
ある state が変わったら別の state を更新し、その変化でさらに別の state を更新する、
という形で useEffect を連鎖させてしまうパターンがあります。
例えば、以下のような実装をしたとします。
function Game() {
const [card, setCard] = useState(null);
const [goldCardCount, setGoldCardCount] = useState(0);
const [round, setRound] = useState(1);
const [isGameOver, setIsGameOver] = useState(false);
// 🔴 useEffect の数珠繋ぎ
useEffect(() => {
if (card !== null && card.gold) {
setGoldCardCount(c => c + 1);
}
}, [card]);
useEffect(() => {
if (goldCardCount > 3) {
setRound(r => r + 1);
setGoldCardCount(0);
}
}, [goldCardCount]);
useEffect(() => {
if (round > 5) {
setIsGameOver(true);
}
}, [round]);
useEffect(() => {
alert("Good game!");
}, [isGameOver]);
function handlePlaceCard(nextCard) {
if (isGameOver) {
throw Error("Game already ended.");
} else {
setCard(nextCard);
}
}
// ...
}
なぜuseEffectが不要なのか
この実装には 2 つの問題があります。
1 つ目は 効率の問題 です。
連鎖内の各 setState ごとにレンダリングが走るため、最悪の場合、
setCard → レンダー → setGoldCardCount → レンダー → setRound → レンダー → setIsGameOver → レンダー
と、不要なレンダリングが何度も発生します。
2 つ目は 拡張性の問題 です。
例えば、ゲームの手順を遡る機能を追加した場合、過去の state に戻すだけで
useEffect の連鎖が再び発火してしまい、意図しない動作を引き起こします。
どう改善するか
レンダー中に計算できるものはそこで行い、残りはイベントハンドラの中で処理します。
function Game() {
const [card, setCard] = useState(null);
const [goldCardCount, setGoldCardCount] = useState(0);
const [round, setRound] = useState(1);
// ✅ レンダー中に計算する
const isGameOver = round > 5;
function handlePlaceCard(nextCard) {
if (isGameOver) {
throw Error("Game already ended.");
}
// ✅ イベントハンドラの中でまとめて state を更新する
setCard(nextCard);
if (nextCard.gold) {
if (goldCardCount < 3) {
setGoldCardCount(goldCardCount + 1);
} else {
setGoldCardCount(0);
setRound(round + 1);
if (round === 5) {
alert("Good game!");
}
}
}
}
// ...
}
こうすることで、isGameOver は計算で求められる値として扱い、
その他の state 更新はイベントハンドラの中で一度に行えます。
このパターンで意識すること
useEffect で state の変化を監視して別の state を更新する連鎖は、まずイベントハンドラにまとめられないかを考える。
2-5. 値のリセット・調整目的だけでuseEffectを使わない
props が変わったときに内部 state をリセットしたい、という場面があります。
例えば、プロフィールページで userId を props として受け取っており、
userId が変わるたびにコメント入力欄をリセットしたいとします。
export default function ProfilePage({ userId }) {
const [comment, setComment] = useState("");
// 🔴 props(userId)の変更に応じて useEffect で state をリセットしている
useEffect(() => {
setComment("");
}, [userId]);
// ...
}
これも状況によっては問題ありませんが、
コンポーネント自体を別物として扱いたい のであれば、key を使った方が意図として明確になります。
export default function ProfilePage({ userId }) {
return (
<Profile
userId={userId}
key={userId}
/>
);
}
function Profile({ userId }) {
// ✅ key が変わるとコンポーネントが再マウントされ、state が自然に初期化される
const [comment, setComment] = useState("");
// ...
}
こう書くことで、userId が変わったタイミングでコンポーネントが再マウントされ、
内部 state も自然に初期化されます。
一部の state だけを調整したい場合
すべての state をリセットするのではなく、props の変化に応じて一部の state だけ変えたいケースもあります。
例えば、items リストを props で受け取り、選択中のアイテムを state で管理しているとします。
function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selection, setSelection] = useState(null);
// 🔴 items が変わるたびに selection を useEffect でリセットしている
useEffect(() => {
setSelection(null);
}, [items]);
// ...
}
この場合も、そもそも selection を state として持つ必要があるかを見直します。
選択中のアイテム ID だけを state で持ち、実体は計算で求める形にすれば、useEffect は不要になります。
function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selectedId, setSelectedId] = useState(null);
// ✅ items と selectedId から計算で求める
const selection = items.find(item => item.id === selectedId) ?? null;
// ...
}
このパターンで意識すること
props変更に応じたリセットは key による再マウントを、一部の調整は計算で済まないかをまず検討する。
2-6. 親コンポーネントへの通知にuseEffectを使わない
自コンポーネントの state が変わったときに、useEffect を使って親コンポーネントに通知する、という実装を見かけることがあります。
例えば、トグルコンポーネントで内部の isOn state が変化したときに、
useEffect で親の onChange を呼び出すパターンです。
function Toggle({ onChange }) {
const [isOn, setIsOn] = useState(false);
// 🔴 state の変化を useEffect で親に通知している
useEffect(() => {
onChange(isOn);
}, [isOn, onChange]);
function handleClick() {
setIsOn(!isOn);
}
// ...
}
この場合、まず Toggle が state を更新してレンダリングされ、その後 useEffect が実行されて親の onChange が呼ばれ、親側でも再レンダリングが走ります。
これは useEffect を使わなくても、イベントハンドラの中で直接解決できます。
function Toggle({ onChange }) {
const [isOn, setIsOn] = useState(false);
function handleClick() {
const nextIsOn = !isOn;
setIsOn(nextIsOn);
onChange(nextIsOn); // ✅ イベントの中で親にも通知する
}
// ...
}
こうすることで、Toggle と親コンポーネントの state 更新が同じイベント内で行われ、React のバッチ処理により 1 回のレンダリングで済みます。
さらに、そもそも isOn を自身の state として持つ必要がなければ、親から props として受け取る形にすることもできます。
// ✅ 親が完全に制御する形
function Toggle({ isOn, onChange }) {
function handleClick() {
onChange(!isOn);
}
// ...
}
なぜuseEffectが不要なのか
useEffect で state の変化を監視して親に通知すると、
自コンポーネントのレンダリング → useEffect 実行 → 親の state 更新 → 再レンダリング
という流れになり、余分なレンダリングが発生します。
イベントハンドラの中で親への通知も一緒に行えば、この無駄を避けられます。
このパターンで意識すること
state の変化を親に伝えたくなったら、まずイベントハンドラの中で一緒に更新できないかを考える。
2-7. アプリケーションの初期化にuseEffectを使わない
アプリが読み込まれるときに一度だけ実行したい処理を、トップレベルのコンポーネントの useEffect に書くことがあります。
function App() {
// 🔴 アプリ起動時に一度だけ実行したい処理
useEffect(() => {
loadDataFromLocalStorage();
checkAuthToken();
}, []);
// ...
}
なぜuseEffectが不要なのか
開発環境では Strict Mode によりコンポーネントが 2 回マウントされるため、この処理も 2 回実行されます。例えば、認証トークンの検証が 2 回走ることでトークンが無効になるなど、予期しない問題が起きる可能性もあります。
どう改善するか
アプリの読み込みごとに 1 回だけ実行したい処理であれば、モジュールのトップレベルで実行するか、フラグで制御します。
// ✅ モジュールのトップレベルで実行する
if (typeof window !== "undefined") {
checkAuthToken();
loadDataFromLocalStorage();
}
function App() {
// ...
}
トップレベルのコードは、コンポーネントがインポートされたときに一度だけ実行されます。
このパターンで意識すること
コンポーネントのマウントごとではなく、アプリの起動ごとに実行したい処理は、useEffect の外に出す。
3. まとめ
今回は、React公式の内容をもとに、useEffect を使わなくてよいパターンを整理しました。
useEffect を書く前に、今回記載した 7 つのパターンを思い出して不要かどうか見直してください。
useEffect は、Reactの外側にある仕組みと同期するための手段です。
React公式でも "escape hatch"と位置づけられており、他に適切な方法がないときに使うものです。
今回は代表的なパターンを整理しましたが、React公式ではより詳細なケースも紹介されています。
そちらも確認いただけると、useEffect を使わないパターンをより鮮明に理解できると思います。
前回の記事と合わせて、useEffect を適切に使えるようになっていきたいです。