目次
はじめに
ReactのHookの中でも使用頻度の高いuseEffect。サンプルなど見ながら何となく使ってきたが、それでは心許なくなってきた。そこで公式ドキュメントを読みながら自分の理解を整理してみた。
筆者なりのuseEffectの理解は「Reactコンポーネントの中で、何らかの処理を、特定の条件で、特定のタイミングで、コールバックとして実行するための仕組み」というもの。何らかの処理は自分が書くものなので良いとして、その実行条件と実行タイミングについては、どのようなものか理解しておく必要がある。
本題に入る前に …
以前の公式ドキュメントでは、「副作用 (effect) フック により、関数コンポーネント内で副作用を実行することができるようになります」とだけ書かれていて、「副作用って意図しない動作ってこと? なんでそんなものが必要なの?」と、筆者としてはかなり混乱してしまった。その理解のためにReactコンポーネントの純粋性や副作用との関係について考察してみたので、参考までに …

コールバックの登録
useEffectは、Reactコンポーネントの関数定義の中に書かれる。つまりコールバックの登録は、Reactコンポーネントが呼び出された時に行われる。(当たり前といえば当たり前だが …)
ならばReactコンポーネントはいつ呼び出されるのか。それは下記のタイミングとなる。
- 上位のコンポーネントから呼び出された時
- 上位のコンポーネントのJSX定義でネストされている
- コンポーネントのステートが変更された時
- マウスなどのUIイベント、ネットワークI/Oの完了イベントなど
他にはコンテキストの更新もトリガーとなる。
コールバックの実行タイミング
useEffectに登録したコールバックが実行されるのは画面の描画後となる。
ドキュメントと簡単なテストプログラムから、レンダリングは次のようなサイクルで実行されていると推測した。 この後で説明するuseLayoutEffectについても合せて記載した。(公式ドキュメントで確認できた訳では無いので、大間違いしている可能性もある …)

プログラムコードの中で行うのはJSXの生成まで。そのあとはReactのランタイムの中で処理される。useEffectや後述するuseLayoutEffectで登録されるコールバックの実行はランタイムの中である。
コールバックの実行条件
前述したように、useEffectのコールバックが実行されるのは、Reactコンポーネントが呼び出され、Reactによる画面の描画が行われたあとになる。ただし毎回の描画後に必ず呼び出される訳ではなく、呼び出しの条件を指定することができる。
- 毎回必ず
- 初めてコンポーネントが呼び出されたとき (初回限り)
- 指定した条件のとき
「毎回必ず」は要注意。コールバックでステートを変更すると、それにより再描画がおこり、コールバックが再び呼び出される。もしステートの更新が続くとコールバックの呼び出しが無限ループとなってしまう。これは「指定した条件のとき」も、同様のことが起こり得る。
条件の指定は、useEffect()を呼び出す時の第2引数に指定する。
- 何も指定しなければ毎回必ず
- 空の配列を指定すると初回限り
- 空でない配列を指定すると、その何れかの要素が更新されたとき
- 前回 useEffect()が呼び出された時に渡された値と異なるとき
- 要素を監視している訳ではないので、useEffect()の呼び出し後の変更は検知されない
- 要素にオブジェクトを指定した場合、その中身の変更は検知されない (JavaScriptのオブジェクトとして同値であれば変化なしと見做される)
- 前回 useEffect()が呼び出された時に渡された値と異なるとき
にそれぞれコールバックが呼び出される。
function MyComponent() {
useEffect(() => {
// 毎回呼ばれる
});
useEffect(() => {
// 初回のみ呼ばれる
}, []);
useEffect(() => {
// someConditionに変更があった時のみ呼ばれる
}, [ someCondition ]);
return <> ... </>;
}
コールバックからのUIの更新
useEffectのコールバックでは、JSXによるUIを直接*変更できないが、DOMを直接操作することでUIを変更できる。ただしReactの設計思想として、DOMの直接操作はできるだけ避けるべしとなっている。
* 直接は変更できなくても、後述するようにステートを更新することで、間接的に変更することはできる
フリッカーの発生
useEffectでDOMを直接更新すべきでない理由のひとつとしてフリッカーの発生がある。コンポーネントの通常処理で画面が描画されたあとに、更に画面を更新することで、短時間に表示が変化することになり、それがフリッカーとなる。このフリッカーを防ぐには、後述するuseLayoutEffectを使用する。
ステートの更新によるUIの更新
「useEffectのコールバックではJSXによるUIの変更はできない」と書いたが、実際にはコールバックの処理結果で画面を更新したい場合がある。その場合はステートを更新して、コンポーネントの再呼び出しを行わせる。その呼出の中で、コールバックの処理結果 (例えばサーバーから取得したデータ) を使ってJSXを生成すればよい。
useLayoutEffectの使用
DOMを直接操作してUIを更新したい場合は、useLayoutEffectを使用する。useLayoutEffectはuseEffectとよく似ているが、違いは呼び出されるタイミングにある。
useLayoutEffectのコールバックは、JSXからDOMを構築・更新したあと、描画の前に呼び出される。useEffectと違い、描画前にDOMを変更できるため、フリッカーを生じることがない。DOM上の要素はリファレンス (useRef) を使ってアクセスする。
useLayoutEffectのコールバックは、その処理が終了するまで、画面が描画されない (ブロックされる)。マウスやキーボードにも反応しない。このためコールバックの処理に時間がかかると、遅いUIになってしまうので注意する。
複数のuseEffectの使用
useEffectは複数定義できる。useEffectの実行条件ごとに独立したuseEffectを定義したり、処理内容に応じて複数のコールバックを用意したりできる。
コールバックの後処理
useEffectのコールバックで何らかの処理を開始して、その後始末をしたい場合がある。この場合は、コールバックから後処理の関数 (以下クリーンアップ) を返してやればよい。クリーンアップは、次のサイクルでコールバックが呼ばれる前に実行される。つまり何らかのトリガーで再描画が発生した場合に実行される。もし再描画が発生しなければ、クリーンアップは実行されない。ただし最後にコンポーネントが破棄される時はクリーンアップが実行される。
クリーンアップが何時呼ばれるかは、次のトリガーの発生次第なので、一定時間内に確実に実行する必要のある処理 (例えばリモートリソースに対するセッションのクローズ) は、クリーンアップには記述できない。
この点では後処理というよりも、新しくコールバックを呼び出す前処理として、以前のコールバックの残滓をクリアする、と位置付けた方がしっくりくるかも。
次のサンプルでは、SubComponent1とSubComponent2の2つのコンポーネントが定義されており、対応するボタンをクリックすることで、表示するコンポーネントを切り替えている。
それぞれのコンポーネントでは、useEffectが呼び出され、そのコールバックはクリーンアップを返すようになっている。
const useState = React.useState;
const useEffect = React.useEffect;
const useLayoutEffect = React.useLayoutEffect;
function App() {
const [select1, setSelect1] = useState<boolean>(true);
console.log('Rendering App - ', (select1 ? 'SubComponent #1' : 'SubComponent #2'));
return (
<>
<button onClick={(e) => setSelect1(true)}>SubComponent #1</button>
<button onClick={(e) => setSelect1(false)}>SubComponent #2</button>
<br /><br />
{select1 && <SubComponent1 />}
{!select1 && <SubComponent2 />}
</>
);
}
function SubComponent1() {
console.log('Rendering SubComponent #1');
useEffect(() => {
console.log('> useEffect #1');
return () => {console.log('> Cleanup SubComponent #1')};
});
useLayoutEffect(() => {
console.log('> useLayoutEffect #1');
});
return (
<>
<p>SubComponent #1</p>
</>
);
}
function SubComponent2() {
console.log('Rendering SubComponent #2');
useEffect(() => {
console.log('> useEffect #2');
return () => {console.log('> Cleanup SubComponent #2')};
});
useLayoutEffect(() => {
console.log('> useLayoutEffect #2');
});
return (
<>
<p>SubComponent #2</p>
</>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
結果は次のようになる。各回のレンダリングで、対象のコンポーネントの描画に先立ち、前回のレンダリングに対するクリーンアップが実行されている。
"Rendering App - " "SubComponent #1"
"Rendering SubComponent #1"
"> useLayoutEffect #1"
"> useEffect #1"
// ボタンSubComponent #2をクリック
"Rendering App - " "SubComponent #2"
"Rendering SubComponent #2"
"> useLayoutEffect #2"
"> Cleanup SubComponent #1"
"> useEffect #2"
// ボタンSubComponent #1をクリック
"Rendering App - " "SubComponent #1"
"Rendering SubComponent #1"
"> useLayoutEffect #1"
"> Cleanup SubComponent #2"
"> useEffect #1"
async/await
useEffectのコールバックでasync/awaitを使うには次のように記述する。
useEffect(() => {
doSomething(...);
}
async doSomething(...) {
await doAsync(...);
}
筆者はTypeScriptを使っており、最初下記のように記述したが、useEffectはPromiseの戻り値を受け付けないと、TypeScriptに怒られてしまった。
// エラーが発生する書き方
useEffect(async () => {
await doAsync(...);
}
JavaScriptであれば、上記の書き方でもエラーにはならない。
不必要なuseEffectの利用を回避
下記の記事には、useEffectを使う必要のないケースについての解説がある。”Removing unnecessary Effects will make your code easier to follow, faster to run, and less error-prone.” とのこと。

その他 (雑感)
筆者の場合、リモートサービスからのデータの読み出しコードをuseEffectに書くことが多い。ただこれは非同期処理なので、画面の描画後という起動タイミングは重要ではない。起動タイミングだけを見ればメインロジックに記述しても問題ない。ただ起動条件の設定は、useEffectを使うと簡単にできるので、useEffectを使うメリットを感じている。
更新記録
日付 | 内容 |
2024/05/01 | 「なぜサイドエフェクトなのか?」の記述を更新 |
2023/09/15 | 「不必要なuseEffectの利用を回避」を追加 |
2023/09/12 | コールバックの後処理のサンプルを置き換え |
2023/08/15 | 文言微調整 |
2023/06/01 | サイドエフェクトの意味についての考察を更新 コンポーネントが廃棄される際にもコールバックの後処理が行われることを追記 |
2022/12/07 | コールバックの実行条件のうち「指定した条件」について補足 |
2022/11/10 | 初版リリース |