ReactのuseEffectとレンダリング
こんにちは。
今年度でNOBORI入社2年目で開発を行っている山中です。
NOBORIの医療機関向けの製品の一つにAIに関わる製品があり、その製品の中のGUIでReactを使用しています。
そのReactのHooksAPIの中で、使用頻度が高く少し複雑なuseEffectの確認をしながらレンダリングの動作について調査しました。
useEffectとは
レンダリング後に実行されるHooksAPIです。
以下4パターンの実行タイミングを設定できます。
1. 毎回実行
2. 初回のマウント(DOMノードの追加)時のみ実行
3. 特定の値を監視し、状態の変化があった場合に実行
4. アンマウント(コンポーネントの破棄)時に実行
レンダリング動作確認
今回の調査の基本となるコードです。
ボタンをクリックすると表示されているカウントが増加します。
const Count = () => {
const [count, setCount] = useState(0)
const onClick = () => {
setCount(count + 1)
console.log("count: クリックされました")
}
return (
<div>
{count}回クリックされました。
<button onClick={onClick}>ボタン</button>
{console.log("count: 描画されました")}
</div>
)
}
上記コードのレンダリング順序
1. ページを表示する。
2. log:「描画されました」
3. ボタンをクリックする。
4. log:「count: クリックされました」
5. log:「count: 描画されました」
=> countの状態が0から1へ変化するため再描画される
1. 毎回実行
useEffectを毎回実行する場合は、useEffectの第二引数を指定しません。
useEffect内で状態を変化する処理を入れると状況によっては無限ループになる可能性があるので注意してください。
基本のコードに下記を追加してレンダリング動作を確認します。
useEffect(() => {
console.log("count: " + count)
})
上記コードのレンダリング順序
1. ページを表示する。
2. log:「count: 描画されました」
3. log:「count: 0」
4. ボタンをクリックする。
5. log:「count: クリックされました」
6. log:「count: 描画されました」
7. log:「count: 1」
=> 描画後にuseEffectが実行されている
8. ボタンをクリックする。
9. log:「count: クリックされました」
10. log:「count: 描画されました」
11. log:「count: 2」
2. 初回のマウント(DOMノードの追加)時のみ実行
useEffectを初回のみ実行する場合は、第二引数に空配列を指定します。
基本のコードに下記を追加してレンダリング動作を確認します。
useEffect(() => {
console.log("count: " + count)
}, [])
上記コードのレンダリング順序
1. ページを表示する。
2. log:「count: 描画されました」
3. log:「count: 0」
=> 初回のマウント後にuseEffectが実行されている
4. ボタンをクリックする。
5. log:「count: クリックされました」
6. log:「count: 描画されました」
=> 初回マウント時以外は再描画されてもuseEffectは実行されない
3. 特定の値を監視し、状態の変化があった場合に実行
useEffectで特定の値に変化があった場合のみ実行したい場合は、第二引数の配列に監視したい値を指定します。複数指定することも可能です。
基本のコードのonClickの修正とuseEffect処理の追加を行い、レンダリング動作を確認します。
useEffect(() => {
console.log("count: " + count)
}, [count])
const onClick = () => {
if(count == 0){
setCount(count + 1)
}
console.log("クリックされました!")
}
上記コードのレンダリング順序
1. ページを表示する.
2. log:「count: 描画されました」
3. log:「count: 0」
4. ボタンをクリックする。
5. log:「count: クリックされました」
6. log:「count: 描画されました」
7. log:「count: 1」
=>監視対象のcountが0から1ヘ変化したためuseEffectが実行される
8. ボタンをクリックする。
9. log:「count: クリックされました」
=>監視対象のcountが変化しないためuseEffectは動作しない
10. ボタンをクリックする。
11. log:「count: クリックされました」
4. アンマウント(コンポーネントの破棄)時に実行
アンマウント時に実行される関数はクリーンアップ関数と呼ばれており、useEffect内のreturnにコールバック関数を指定することで実行できます。
基本のコードに下記を追加してレンダリング動作を確認します。
useEffect(() => {
console.log("count: マウントされました")
return () => {console.log("count: アンマウントされました")}
})
上記コードのレンダリング順序
1. ページを表示する.
2. log:「count: 描画されました」
3. log:「count: マウントされました」
4. ボタンをクリックする。
5. log:「count: クリックされました」
6. log:「count: 描画されました」
7. log:「count: アンマウントされました」
8. log:「count: マウントされました」
5. コンポーネントを入れ子にする
最後にコンポーネントを入れ子にしてみます。
Count: 親コンポーネント
Hello: 子コンポーネント
とします。
Countと同様にボタンをクリックすると「helloWorld」と表示されるコンポーネントを用意し、基本のコードのCountもコンポーネントが入れ子になるように修正します。
const Hello = () => {
const [text, setText] = useState('')
useEffect(() => {
console.log("hello: マウントされました")
return () => {console.log("hello: アンマウントされました")}
}, [text])
const onClick = () => {
setText('helloWorld')
console.log("hello: クリックされました")
}
return (
<div>
{text}
<button onClick={onClick}>helloボタン</button>
{console.log("hello: 描画されました")}
</div>
)
}
const Count = () => {
...
useEffect(() => {
console.log("count: マウントされました")
return () => {console.log("count: アンマウントされました")}
})
return (
<div>
{count}回クリックされました。
<button onClick={onClick}>ボタン</button>
{console.log("count: 描画されました")}
<Hello />
</div>
)
}
上記コードのレンダリング順序
1. ページを表示する.
2. log:「count: 描画されました」
3. log:「hello: 描画されました」
4. log:「hello: マウントされました」
5. log:「count: マウントされました」
6. countボタンをクリックする。
7. log:「count: クリックされました」
8. log:「count: 描画されました」
9. log:「hello: 描画されました」
10. log:「count: アンマウントされました」
11. log:「count: マウントされました」
12. helloボタンをクリックする。
13. log:「hello: クリックされました」
14. log:「hello: 描画されました」
15. log:「hello: アンマウントされました」
16. log:「hello: マウントされました」
まとめ
useEffect
//1. 毎回実行
useEffect(() => {})
//2. 初回のマウント(DOMノードの追加)時のみ実行
useEffect(() => {}, [])
//3. 特定の値を監視し、状態の変化があった場合に実行
useEffect(() => {}, [xxx])
//4. アンマウント(コンポーネントの破棄)時に実行
useEffect(() => {return () => {}})
レンダリングされるパターン
・値が変化した時にコンポーネントが再描画される。
・親コンポーネントがレンダリングされると、子コンポーネントも状態の変化に関わらず再描画される。ただしクリーンアップ関数は実行されない。
レンダリングされないパターン
・コンポーネント内の状態に変化がない場合
・新規の値をセットしても、同じ値の場合にはレンダリングされない
・子コンポーネントの状態が変化した場合は、親コンポーネントはレンダリングされず、子コンポーネントのみが再レンダリングされる。