ReactのuseEffectフック: 初心者向けの詳細な解説と複数のuseEffectの扱い方
今回は、Reactにおける重要な概念である「副作用フック」の一つであるuseEffect
について、初心者の方に向けて詳しく説明したいと思います。
Reactをマスターするためには、useEffectを含む副作用フックの理解は必須です。
しかし、useEffectの機能を完全に理解することは容易ではありません。
特にReact初心者の方にとって、このフックが引き起こす副作用を処理することは、やや面倒で難しいと感じるかもしれません。
しかし、経験を積んでいくうちに、すべてが論理的に結びついていることに気付くでしょう。
useEffectフックを深く学ぶことは、将来のあなたの能力向上に必ず役立つでしょう。
私は、自身の経験と情報をもとに、今後の学習に役立つuseEffectに関する詳細な解説を提供します。
useEffectおさらい
軽くおさらいを致しましょう。
主なuseEffect機能の効果は以下となります。
・ データ取得(フェッチ)
・ローカルストレージからの読み込み
・イベントリスナーなどの追加など
・ 状態またはPropsが変更された時の動作を実行
・コンポーネントのアンマウント時にイベントリスナーをクリーンアップする
これらは一般的な使用例となっております。
useEffectフックは、Reactライフサイクルイベントの部分的な代替品となります。
つまり、クラスコンポーネントでの頻繁に使用されるライフサイクルメソッドである下記を行動複製することができます。
componentDidMount
componentDidUpdate
componentWillUnmount
useEffectフックを含むコンポーネントの変更に対応が可能となります。
そしてuseEffectフックは、コードの重複を減らします。
これは、公式のReactドキュメントでは、1つのステートメントでライフサイクルメソッドの結果として生じる、重複したコードを回避できることが提唱されています。
useEffectフックは2つの引数を取り1番目の引数はデフォルトですべてのレンダリング後に実行されるコールバックとなり
2番目の引数では、ターゲットの状態に変化があった時のみにコールバックされるフックとなります。
useEffect((callback) => { // 副作用 }, [testCode1, testCode2]);
そしてオプションはDependency(依存性)配列です。
依存関係である以前の状態である値
と現在の状態の値
を比較します。
useEffect(() => { // 副作用 }, [state1, state2, state3]);
複数の状態変数が副作用に影響する場合は、それらを依存関係配列にカンマ区切りで渡すことができます。
そして、2つの引数である値が一致しなかった場合に関しては、最初の引数で指定された値がコールバックとして実行されます。
依存関係は、useEffectの2番目の引数として指定するものとなり、依存関係を指定した場合、useEffectはその依存関係が変更されたときにのみトリガーされていきます。
依存関係配列ではデフォルトでのコールバック動作をオーバーライドします。
フックがスコープ内の他のすべてを無視するようになります。
ですので、useEffectフックでは依存関係配列を正しく使用することが最も重要となります。
例えば、何も変更されていない場合でも不要な再レンダリング
を防ぐことも重要となります。
そして、この副作用フックは再レンダリングが起こる度に実行されていきます。
依存関係配列を使用した場合、渡された依存関係が変更度に繰り返し実行されます。
空の配列では、フックが再び実行されないように無限ループの問題を解決しAPIの呼び出しが可能となり、ライフサイクルメソッドとは対照的で、非同期として実行されるのでUIをブロック致しません。
このように、useEffectフックは、さまざまな状況に応じて使用できるとう事です。
複数でのuseEffect
useEffectで複数の副作用がある場合では、あなたはどう書きますか?
以下は、複数のuseEffectを一つのuseEffectに組み合わせたコードです。
import React, { useState, useRef, useEffect } from "react"; const App = () => { const [Render, setRender] = useState("default value"); const renderRef = useRef(); useEffect(() => { console.log("useEffect"); document.Render = Render; console.log("local storage"); const useLocal = localStorage.getItem("local"); setRender(useLocal || []); }); console.log("render message"); const handleClick = () => setRender(renderRef.current.value); return ( <div> <input ref={renderRef} /> <button onClick={handleClick}>Click here</button> </div> ); }; export default App;
上記はあまり最善の方法とは言えません。
複数での副作用の場合useEffectは1つにまとめて組み合わせるのではなく、useEffectを複数呼び出し分けなければいけません。
まず、無限ループとして実行されています、これは複数か組み合わせなのかではありません。
原因は、useEffect内の第2引数の依存配列が指定されていないためです。
この副作用フックであるuseEffectを複数呼び出し使用した場合でも、バグやパフォーマンスの問題を引き起こし無限ループとして実行されます。
// App.js import { useEffect, useState, useRef} from 'react'; const App = () => { const [Render, setRender] = useState(" default value"); const renderRef = useRef(); useEffect(() => { console.log("useEffect"); document.Render = Render; }); useEffect(() => { console.log("local storage"); const useLocal = localStorage.getItem("local"); setRender(useLocal || []); }); console.log("render message"); const handleClick = () => setRender(renderRef.current.value); return ( <div> <input ref={renderRef} /> <button onClick={handleClick}> Click here </button> </div> ); } export default App;
DEMOの展開
useEffectでの全ての状態が変化した時にsetRender
は他がトリガーされ状態は再度、更新され続け無限ループとなってしまいます。
useEffectで依存関係配列を指定しなかった場合は、全ての機能のuseEffectが実行されます。
コード内の位置に基づき、次々と実行されることを意味しています。
データの変更
やPropsの変更
およびユーザーがコンポーネントに対して確認した時など、特定の条件後に副作用を実行しなければいけません。
useEffectでこのような事が起きた場合では、まずしなければいけない事不要な再レンダリングを抑える事です
。
// 不要な再レンダリングを防ぐ const [Render, setRender] = useState("default value"); const renderRef = useRef(); useEffect(() => { console.log("useEffect"); document.Render = Render; },[Render, setRender]); //無限ループ回避 useEffect(() => { console.log("local storage"); const useLocal = localStorage.getItem("local"); setRender(useLocal || []); }, [setRender]);
コンソールで確認して下さい。
再レンダリングが起きた後に不要なのをスキップさせたい時は、このように依存関係として配列を追加させてあげます。
useEffect(() => { console.log("useEffect"); document.Render = Render; },[Render, setRender]); //無限ループ回避
また、2番目のuseEffectフックの依存関係の配列には空の配列を渡していますが、これによって初回のマウント時のみ実行されることを意図している場合は問題ありません。
useEffect(() => { console.log("local storage"); const useLocal = localStorage.getItem("local"); setRender(useLocal || []); }, []); //第一引数が実行される
ただし、依存関係の配列を空にすると、setRender
関数が更新された場合に再度実行されなくなるため、setRender
を含めることが重要です。
useEffect(() => { console.log("local storage"); const useLocal = localStorage.getItem("local"); setRender(useLocal || []); }, [setRender]);
注意しなければいけないのが、この依存関係を省略したりして書いたりすると、バグが発生する可能性が非常に高くなる事です。
以下は、修正した完全なコードです。
import React, { useState, useRef, useEffect } from "react"; const App = () => { const [Render, setRender] = useState("default value"); const renderRef = useRef(); useEffect(() => { console.log("useEffect"); document.Render = Render; }, [Render, setRender]); useEffect(() => { console.log("local storage"); const useLocal = localStorage.getItem("local"); setRender(useLocal || []); }, [setRender]); console.log("render message"); const handleClick = () => setRender(renderRef.current.value); return ( <div> <input ref={renderRef} /> <button onClick={handleClick}>Click here</button> </div> ); }; export default App;
修正点は以下の通りです
最初のuseEffectフックの依存関係の配列に、Render
とsetRender
を含めました。これにより、Render
の値またはsetRender
関数が更新されるたびにuseEffectが実行されます。
2番目のuseEffectフックの依存関係の配列にもsetRender
を含めました。これにより、setRender
が更新された場合に再度実行されるようになります。
useEffectのコールバック関数は非同期的に実行されるため、コンソールログの順序は保証されませんので、注意が必要です。
正確な順序を確認するには、各useEffectフックの最初の行にログを追加し、それぞれの実行を識別する必要があります。
修正したコードでは、不要な再レンダリングが抑えられ、各useEffectフックが適切に動作するようになっています。
このように、複数のuseEffectを独立した呼び出しとして使用することが推奨される場合があります。
これは、各useEffectの役割やタイミングが異なる場合や、依存関係がない場合に適しているからです。
この方法の利点はいくつかあります。
まず第一に、各useEffectの目的や処理内容が明確になります。複数の処理が混在している場合よりも可読性が高まります。
また、useEffectの依存関係を明示的に指定することで、不要な再実行や無限ループを防ぐことができます。
さらに、複数の独立したuseEffectを使用することで、テストやデバッグの容易さが向上します。
各useEffectが独立しているため、特定の処理を単独で検証することが容易になります。
ただし、必ずしも複数のuseEffectを独立して使用する必要はありません。場合によっては、複数の処理が密接に関連しており、特定の順序で実行する必要がある場合もあります。
その場合は、適切なタイミングや依存関係を考慮しながら、複数のuseEffectを組み合わせて使用することもあります。
最終的には、状況や要件に基づいて適切な方法を選択する必要があります。
useEffectの使用方法は柔軟であり、最適なアプローチは異なる場合もあります。
重要なのは、コードをより明確にし、目的に合った処理を実行するために適切な方法を選択することです。
クリーンアップ関数
// Appコンポーネントはカウンターの状態を管理します const App = () => { const [count, setCount] = useState(0); // useEffectフックを使用して副作用(インターバルでのカウントアップ)を設定します useEffect(() => { const interval = setInterval(() => { setCount((value) => value + 1); }, 1000); }, []); return <p>counter {count}</p>; } // EffectUnmountコンポーネントはカウンターのアンマウントを制御します const EffectUnmount = () => { const [unmount, setUnmount] = useState(false); // Rrender関数はアンマウントされていない場合にCounterコンポーネントを返します constRrender = () => !unmount && <Counter />; return ( <div> <button onClick={() => setUnmount(true)}>Unmount counter</button> {Render()} </div> ); } export default App;
上記のコードにはいくつかの誤りがあります。
まず、constRrender
という関数名が存在しますが、正しくはRender
となります。
<Counter />
コンポーネントが存在しません。Counter
コンポーネントが定義されていないため、エラーが発生します。
そして、Appコンポーネント内のuseEffectフックがクリーンアップの必要性があることです。
具体的には、setInterval
関数によって生成されたインターバルをクリアするために、clearInterval
を実行する必要があります。
これは、コンポーネントがアンマウントされる前に行われるクリーンアップの手続きです。
上記のコードでは、クリーンアップの手続きが不足しているため、それを追加して正しく動作するようにする必要があります。
// Appコンポーネントはカウンターの状態を管理します const App = () => { const [count, setCount] = useState(0); // useEffectフックを使用して副作用(インターバルでのカウントアップ)を設定します useEffect(() => { const interval = setInterval(() => { setCount((value) => value + 1); }, 1000); // コンポーネントがアンマウントされる際にクリーンアップを行います return () => { clearInterval(interval); }; }, []); return <p>counter {count}</p>; } // EffectUnmountコンポーネントはカウンターのアンマウントを制御します const EffectUnmount = () => { const [unmount, setUnmount] = useState(false); // Render関数はアンマウントされていない場合にCounterコンポーネントを返します const Render = () => !unmount && <Counter />; return ( <div> <button onClick={() => setUnmount(true)}>Unmount counter</button> {Render()} </div> ); } export default App;
上記のコードでは、クリーンアップ関数を useEffect フックの戻り値として提供しています。これにより、コンポーネントがアンマウントされる際に実行されるクリーンアップ関数が定義されます。
つまり、Appコンポーネント内のuseEffectフックが正しくクリーンアップされます。
また、EffectUnmount
コンポーネント内で<App />
をレンダリングしています。
ですが、アンマウント時に毎回ではなく一度だけ呼びたい場合です。
アンマウント時に一度だけクリーンアップ関数を呼びたい場合は、依存関係の配列に値を指定する必要があります。
以下に修正したコードとそれに基づく説明を示します。
import { useState, useEffect } from 'react'; // Appコンポーネントはカウンターの状態を管理します const App = () => { const [count, setCount] = useState(0); // useEffectフックを使用して副作用(インターバルでのカウントアップ)を設定します useEffect(() => { const interval = setInterval(() => { setCount((value) => value + 1); }, 1000); // クリーンアップ関数はアンマウント時に一度だけ呼び出されます return () => { clearInterval(interval); }; }, [count]); // countを依存関係の配列に追加 return <p>Counter: {count}</p>; }; // EffectUnmountコンポーネントはAppコンポーネントのアンマウントを制御します const EffectUnmount = () => { const [unmount, setUnmount] = useState(false); // Render関数はアンマウントされていない場合にAppコンポーネントを返します const Render = () => { if (!unmount) { return <App />; } else { return null; } }; return ( <div> <button onClick={() => setUnmount(true)}>Unmount counter</button> {Render()} </div> ); }; export default EffectUnmount;
上記の、修正されたコードでは、AppコンポーネントのuseEffectフックの依存関係の配列にcount
を追加しています。
これにより、初回のレンダリング後にクリーンアップ関数が呼び出され、その後の再レンダリング時には前回のレンダリング時のcount
の値と現在のcount
の値を比較して、変化がある場合のみクリーンアップ関数が呼び出されます。
これにより、アンマウント時に一度だけクリーンアップ関数が呼び出されることが保証されます。
クリーンアップ関数は、副作用のクリーンアップやリソースの解放などの処理を行うのに適しています。
EffectUnmount
コンポーネントでは、Render
関数がアンマウントされていない場合にAppコンポーネントを返します。Render
関数内の条件分岐によって、unmount
の値がtrue
になるとnull
を返してAppコンポーネントがアンマウントされるようになります。
useEffectフック内で複数の関数を実行することは問題ありません。それらの関数は同時に実行されます。
ただし、マウント時にリクエストをクリーンアップするためには、リクエストが終了する前にクリーンアップが実行される可能性があることに注意する必要があります。
それは、後のリクエスト処理が先に早く終了する場合がある為です。
適切な制御構造を使ってクリーンアップのタイミングを調整する必要があります。
最後に
useEffectフックは依存関係配列を使用してエフェクトの実行をコントロールします。
依存関係の1つが変更されるたびに、エフェクトが実行されていき、ほとんどの場合は状態が変化するたびに、一度だけではなくエフェクトを実行するようにコンポーネントを作成していく必要があるのです。
useEffectフックの基礎的な作成概念を理解することは、React開発者になりたい場合に習得するための次のステップへと行く重要なスキルとなっていると思っております。
本日は以上となります。
最後まで読んで頂きありがとうございます。
あなたのReactスキルが、次にステップアップされるのに当記事が役立つ事を願います。
この記事が、役に立ったら、ブックマークと共有をしていただけると幸いです。