なぜこの記事を書こうと思ったのか
以前書いた記事で、初級から中級者においてのReactにおける「メモ化」がなんなのかという点はおおよそ記事を書いて理解が進んだなぁという印象。
ここでまた割と理解度が同一ぐらいなんじゃないかという、react-hooksにおける「第二引数の依存配列」というものに関して深掘りして調べてみようと思い今回記事を書きました。
まずはよく聞く、「useEffectの副作用」ってなんなのかと
「コンポーネントのレンダリングの外で起こる処理のこと」
-> レンダリングに関係ないのに、何かしら影響を与える処理のことを指す。
「レンダリングに関係のあること」
- レンダリング(UI更新)だけをしている
- 外部に影響を与えない
「レンダリングに関係ないこと」
- レンダリングに関係ないこと(API通信)
- コンポーネントのレンダリングに直接関係ない処理
なぜReactではuseEffectで副作用を扱うのか?
Reactのコンポーネントは「レンダリングするだけのピュアな関数であるべき」という考え方があるため。
副作用(API通信・イベント登録・DOM操作など)はレンダリングのたびに実行されると問題が起きる。
- APIを無限に呼び続ける
- イベントリスナーが無限に増える
だから、React では「副作用を useEffect にまとめて管理する」設計になっている。
ようするに非常に高速でレンダリングが実行されるReactにおいて、「何度も実行する必要がないもの」は「副作用」として取り扱おうね!ということみたい。なるほど。
ちなみにReactにおける副作用を再度、おさらい含んで書いてみると
- APIリクエスト
- DOMの変更
- イベントリスナーの登録
- タイマー系の処理
- ローカルストレージの操作
となるらしい。
誤ったuseEffectの使い方
2個ある入力欄のvalue値をuseEffectで第二引数に持たせて、変更の変更を監視、変更がされるごとにuseEffectの関数を実行しているケース。
これでも動くと言えば動くが、「名前とハンドルネームが一致しているものか」というロジックはレンダリングに直結するものなのにuseEffectの中で管理してしまっていることが問題。(ちなみに一応だが、「動作確認的には問題ない」)
import React, { ChangeEvent, useEffect, useState } from "react";
const App = () => {
const [name, setName] = useState("やまだ");
const onChangeName = (e: ChangeEvent<HTMLInputElement>) => {
setName(e.target.value);
};
const [handleName, setHandleName] = useState("たろう");
const onChangeHandleName = (e: ChangeEvent<HTMLInputElement>) => {
setHandleName(e.target.value);
};
const [namesAreSame, setNamesAreSame] = useState(false);
useEffect(() => {
if (name === handleName) return setNamesAreSame(true);
return setNamesAreSame(false);
}, [name, handleName]);
return (
<div className="bg-gray-400 h-lvh flex flex-col gap-2">
<p>名前</p>
<input type="text" value={name} onChange={onChangeName} />
<p>ハンドルネーム</p>
<input type="text" value={handleName} onChange={onChangeHandleName} />
{namesAreSame && <p>名前とハンドルネームが重複しているよ</p>}
</div>
);
};
export default App;
今の時点でなんとなくではありますが自分もレンダリング周りh理解してるので、たしかにこの書き方をされてレビューを依頼された場合は「なんや?このuseEffect?? 外部通信でもしとるんか??」となりそうな感じはしました。
上記のコードを直してみる
import React, { ChangeEvent, useEffect, useState } from "react";
const App = () => {
const [name, setName] = useState("やまだ");
const onChangeName = (e: ChangeEvent<HTMLInputElement>) => {
setName(e.target.value);
};
const [handleName, setHandleName] = useState("たろう");
const onChangeHandleName = (e: ChangeEvent<HTMLInputElement>) => {
setHandleName(e.target.value);
};
const namesAreSame = name === handleName;
return (
<div className="bg-gray-400 h-lvh flex flex-col gap-2">
<p>名前</p>
<input type="text" value={name} onChange={onChangeName} />
<p>ハンドルネーム</p>
<input type="text" value={handleName} onChange={onChangeHandleName} />
{namesAreSame && <p>名前とハンドルネームが重複しているよ</p>}
</div>
);
};
export default App;
入力内容が変更されるたびにレンダリングによるnamesAreSameが計算されるため、適切なメッセージ出力が可能となった。
ひとまず、第二引数が変更された時点でuseEffectが実行される、という点の理解で問題はないみたい
const [isOpen, setIsOpen] = useState(false);
useEffect(() => {
console.log("isOpen が変わったので実行");
}, [isOpen]);
return <button onClick={() => setIsOpen((prev) => !prev)}>トグル</button>;
useEffectは第二引数が指定されているケースでも、初回マウント時点で実行されるようになっている。
今までの自分の理解としては以下でした
- useEffectは第二引数が空文字の場合は、「初回マウント時にのみ実行される」 -> ⭕️
- useEffectは第二引数が指定されている場合は、「依存配列に指定された内容が変更されたケース」でのみ実行される -> ❌
ただしくは、
- useEffectは第二引数が空文字の場合は、「初回マウント時にのみ実行される」 -> ⭕️
- useEffectは第二引数が指定されている場合は、「初回マウント時と第二引数が変更された時点で実行される」 -> ⭕️
ようです。なので初回マウントした時点で以下のログ出力でconsoleに文字が出力されていることがわかります。
(余談)初回マウント時に処理を実行したくない場合は以下コードのようにuseRefを使用することで解消することが可能。
import { useEffect, useRef, useState } from "react";
const Component = () => {
const [isOpen, setIsOpen] = useState(false);
const isFirstRender = useRef(true); // 初回レンダリングかどうかを判定するフラグ
useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false; // 初回なら何もせずスキップ
return;
}
console.log("isOpen が変わったので実行");
}, [isOpen]);
return <button onClick={() => setIsOpen((prev) => !prev)}>トグル</button>;
};
最後に
学習を開始した時点ではまずぶち当たるレンダリングの壁でしたが、実装経験を踏まえて学んでみて、改めてこの記事内で書いていくうちにあぁ、なんだこれだけかというところでだいぶ腑に落ちたなぁという印象でした。