PR
【React入門】useEffectを初心者〜中級者がキチッとサクッと理解する
Reactを触っていて「わかりにくい」と感じるHooks…。
今回はuseStateに次いでよく使うuseEffectについて説明します。
useEffectは挙動にちょっとクセがあり、ヘタをするとエラーの温床になるので、しっかりとポイントを押さえていきましょう!
useEffectとは
「そもそもHooksとは?」という話はuseStateのところでしているので、そちらをご覧ください。
useEffectとは、「副作用フック」とか言われたりしますが、
(?ふくさよう?)
って感じですよね。
副作用、とは関数コンポーネントの処理に関係ない(=要はWebページのレンダリング(描画)に関係ない)処理のことです。
このレンダリングに関係ない処理を、レンダリングした後に行う、というのがuseEffectになります。
「レンダリングの後に処理を行う」というところがポイントです!
useEffectの基本的な使い方
まずはuseEffectを使うためにインポートしておきます。
import useEffect from "react";
これでuseEffectを使う準備は出来ました。
useEffectの構文
useEffectの基本的な構文は次の通りです。
useEffect(
() => {処理}, //第一引数。処理を関数でかく。
[変数1, 変数2, …] //第二引数。この配列にある変数に変更があるとuseEffectを実行する。
)
とりあえずこの段階ではよくわからなくてもOKです。useEffect は実行されるタイミングが重要になってくるので、次の解説で詳しく説明していきます。
useEffectが実行されるタイミング
ここの理解がuseEffectの一番のポイントといっても過言ではないでしょう。
最初に説明した通り、useEffectの処理はレンダリングされた後に実行されるのですが、具体的にいうと、
- 最初ブラウザに読み込まれたとき。(レンダリング後、だから)
- stateやpropsに変更があって、再レンダリングされたとき。
この2つのタイミングでuseEffectの処理が実行されます。
もう一度先ほどの基本的な構文を見てみましょう
useEffect(
() => {処理}, //第一引数。処理を関数でかく。
[変数1, 変数2, …] //第二引数。この配列にある変数に変更があるとuseEffectを実行する。
)
useEffect内の第一引数「() => {処理}」は、まず最初ブラウザに読み込まれたときに一度実行されます。ここは必ず実行されます。
それ以降、stateやpropsの変更によって再レンダリングされるたびに「() => {処理}」が実行されます。
ただし、この再レンダリングの際の実行については、第二引数の値指定で制御することができます。つまり、再レンダリングされたとしても第二引数に関係ない値更新による再レンダリングであれば実行しない、という制御ができます。
ま、次の例を見てもらった方がわかりやすいでしょ。
useEffectの書き方の例と実行タイミング
consoleにログを表示する、という簡単なコードを作ってみます。
実行タイミングの説明のため、なので全文は載せません。ご了承ください。ここではuseEffectの書き方と実行タイミングがわかればOKです。
…
const [count, setCount] = useState(0);
useEffect(
() => {console.log("useEffectが実行されたよ!");},
);
…
return (
<div className = {styles.main}>
<div>useEffectは{count}回実行されたよ</div>
<button onClick={()=>{setCount(count + 1)}}>再レンダリング!</button>
</div>
)
GIFを見てもらえればわかりますが、まずなにもしていなくてもconsoleに一度「useEffectが実行されたよ!」が表示されています。これは最初ブラウザに読み込まれたときに実行されたuseEffectです。
そのあと、ボタンをクリックするたびにstateが変わって再レンダリングされて、「useEffectが実行されたよ!」が表示されています。
useEffect(
() => {console.log("useEffectが実行されたよ!");},
[]
);
第二引数に空の配列を渡すと、最初ブラウザに読み込まれたときにだけ実行してくれます。
stateの変更で再レンダリングされても実行されていないのがわかると思います。
外部からデータを取ってきてブラウザに表示する、といったときなど、useEffectの実行が最初の一度でいい場合はこのような指定をします。
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
useEffect(
() => {
console.log("useEffectが実行されたよ!");
if(count1 >= 3){
console.log("count1が3以上になっちまったぞ!")
};
},
[count1]
);
return (
<div className = {styles.main}>
<div>count1は{count1}回実行されたよ。count2は{count2}回実行されたよ。 </div>
<button onClick={()=>{setCount1(count1 + 1)}}>カウント1アップ!</button>
<button onClick={()=>{setCount2(count2 + 1)}}>カウント2アップ!</button>
</div>
)
第二引数に変数を指定すると、その変数に変化があったときだけuseEffectが実行されます。
こちらの例ではcount1の値に変化があったときだけuseEffectを実行し、しかもcount1が3以上になるとconsoleに「count1が3以上になっちまったぞ!」というログが表示されます。
第二引数の指定がないと、useEffectは再レンダリングするたびに無差別実行されます。無駄なことも多いですし、処理の内容によってはパフォーマンスの低下、予期せぬ無限ループ、メモリリークなんかを引き起こすこともありますので、第二引数は適切に指定してください。
useEffectはどんなときに使う?
レンダリングされた後に実行する関数…。
って例えばどう使うの?ログの出力だけじゃよくわかんない…。
ということで、具体的な使用例をあげてみます。
useStateと組み合わせて、stateを監視して処理実行
多分一番使う使い方だと思います。
特に入力によってif文の条件判定をする、というときによく使うかな、という感じです。先ほどのGIF動画のような使い方が多いかな、と思います。
ただ、本当にuseEffectが必要か…はよく考えないといけないです。結構「これ、useStateだけで済むじゃん」というケースも多いです。
外部のデータを取得、購読して表示
これはuseEffectの力を借りた方がいいです。
ただ「ネットのサンプルコードでよくやってる」くらいで私自身はこういう実装をしたことありません。
私自身の使い方としては、アニメーションの実装のときや、ブログ記事にアドセンスを配置するときに使っています。
useEffectを使うときはちょっと注意を払わないといけない
useEffectを使い始めると、結構乱用しがちです。
それだけuseEffectは便利なのですが、「レンダリング後に実行される」というのと「レンダリングされるたびに実行される」という性質上、ちょっと注意して使わないといけません。
そもそも、そのuseEffect必要?
useEffect内での処理はなんでもできちゃうので、ついここで色々な処理をしがちです。
が、useEffectはレンダリングされた後に実行されるので、処理の内容によっては、
それ、useEffect内でやらないとダメ?
というようなこともあります。
例えば「性と名を入力してフルネームを表示する」という機能を実装しようとしましょう。
const [firstname, setFirstName] = useState("クマ太郎");
const [lastname, setLastName] = useState("クマ田");
const [fullname, setFullName] = useState("");
//ここのuseEffect、いる?
useEffect(
() => {
setFullName(lastname + " " + firstname);
}
)
const firstnameChange = (e) => {
setFirstName(e.target.value);
}
const lastnameChange = (e) => {
setLastName(e.target.value);
}
…
<div>私の名前は{fullname}!よろしく!</div>
<input type = "text" name = "firstname" value={firstname} onChange = {firstnameChange} />
<input type = "text" name = "lastname" value={lastname} onChange = {lastnameChange} />
…
useEffectを使って、こんな感じのコードを思いついたとしましょう。もちろんこれでも動くのですが…
const [firstname, setFirstName] = useState("クマ太郎");
const [lastname, setLastName] = useState("クマ田");
// const [fullname, setFullName] = useState("");
// useEffect(
// () => {
// setFullName(lastname + " " + firstname);
// }
// )
//これで十分
const fullname = lastname + " " + firstname;
実はこれだけで十分です。入力するたびにsetFirstNameとsetLastNameが実行されて再レンダリングされるので、そのときにfullnameも一緒に更新されます。
useEffectはレンダリングされた後に実行されますし、その内容によっては再度レンダリングをすることもありえます。実際に今回の最初のコードは、useEffect内でstateの更新をしているので、再度レンダリングされます。
useEffectの分だけ再レンダリングが無駄なのがわかります。
本当にuseEffectが必要なのか?をよく検討して、できるならuseEffectに頼らず、シンプルに書いていきましょう。
メモリリークの危険はない?クリーンアップしてる?
useEffectは外部データの購読なんかによく使われます。
そのときに「定期的に外部データ読み込みをする」ようにしていると、放置したままだと、そのページから移動してもその定期的な読み込みが生きたままになります。
そこで、「クリーンアップ関数」といわれる関数を使って、useEffectで設定した処理をアンマウント時に解除してあげます。クリーンアップ関数はアンマウント時と次のuseEffectが実行されるときに実行されます。
useEffectで定期実行するような場合、このクリーンアップ関数で処理してやらないとメモリリークの原因になります。
次の例はsetIntervalでカウントアップの関数を定期実行するuseEffectの例です。
const [ count, setCount ] = useState(0);
useEffect(
() => {
const time = setInterval( // 1秒ごとに定期実行する関数を、最初のレンダリングのときにセット
() => {
console.log("定期実行!");
setCount((prevState) => prevState + 1);
//↑前のstateを使って次のstateを更新。詳しくは、ここの最後「もう少し詳しい説明はこちら」で。
}, 1000
);
return () => clearInterval(time); //クリーンアップ関数。これで定期実行する関数をリセットしてる。
},
[]
);
return (
<div className = {styles.main}>
<div>クマが{count}頭!どんどん増える!</div>
</div>
)
ページ移動したら「定期実行!」のログが止まるのがわかると思います。
それでは、クリーンアップ関数をコメントアウトしてみましょう。
const [ count, setCount ] = useState(0);
useEffect(
() => {
const time = setInterval(
() => {
console.log("定期実行!");
setCount((prevState) => prevState + 1);
}, 1000
);
// return () => clearInterval(time); コメントアウトしてみる
},
[]
);
return (
<div className = {styles.main}>
<div>クマが{count}頭!どんどん増える!</div>
</div>
)
useEffectのクリーンアップ関数が設定されていなかったら、ページ移動しても「定期実行!」が止まらない、つまりsetIntervalが実行され続けていることがわかります。
放っておくとメモリ的によくないですね。
ちなみに、useEffectの使い方を間違えるとすぐにメモリリークしてしまいます。
const [ count, setCount ] = useState(0);
useEffect(
() => {
setInterval(
() => {
console.log("定期実行!");
setCount(count + 1); //ここと、
}, 1000
);
},
[count] //ここを変更
);
return (
<div className = {styles.main}>
<div>クマが{count}頭!どんどん増える!</div>
</div>
)
useEffectの使い方からすると「どこがおかしいの?」という感じがしますが、
ものすごい数の「定期実行!」が呼ばれており、最終的にはメモリリークが起きています。これは、useEffectが実行されるたびにsetIntervalが呼ばれているからです。
こういう意味でもuseEffectは使い方が難しいです。
もう少し詳しい説明はこちら
const [ count, setCount ] = useState(0);
useEffect(
() => {
setInterval(
() => {
console.log("定期実行!");
setCount(count + 1); //ここと、
}, 1000
);
},
[count] //ここを変更
);
return (
<div className = {styles.main}>
<div>クマが{count}頭!どんどん増える!</div>
</div>
)
先ほどのコードですが、問題点は2つです。
- 第二引数にcountを指定している。→つまり、countが変化するたびにuseEffect(=setInterval)を呼び出している。
- そのsetIntervalをクリーンアップする処理を書いていない。
ということで、一秒ごとにどんどん新しいsetIntervalが呼ばれてメモリを食っていくので、メモリリークが起きてしまう、ということになります。
こちらもクリーンアップ関数で処理してあげれば、メモリリークが起きずに済みます。
const [ count, setCount ] = useState(0);
useEffect(
() => {
const time = setInterval( // 名前をつけてあげて…
() => {
console.log("定期実行!");
setCount(count + 1);
}, 1000
);
},
return () => clearInterval(time); // クリーンアップしてあげる
[count]
);
return (
<div className = {styles.main}>
<div>クマが{count}頭!どんどん増える!</div>
</div>
)
ただ、1秒ごとにsetIntervalを呼んで、すぐさまクリーンアップする、というのもなんか無駄な感じですよね。
そこで、最初のコードになります。
const [ count, setCount ] = useState(0);
useEffect(
() => {
const time = setInterval( // 1秒ごとに定期実行する関数を、最初のレンダリングのときにセット
() => {
console.log("定期実行!");
setCount((prevState) => prevState + 1);
//↑前のstateを使って次のstateを更新。ここがこの書き方のポイント。
}, 1000
);
return () => clearInterval(time); //クリーンアップ関数。これで定期実行する関数をリセットしてる。
},
[]
);
return (
<div className = {styles.main}>
<div>クマが{count}頭!どんどん増える!</div>
</div>
)
これなら、setIntervalを呼び出すのは最初の一回。そしてアンマウント(ページ移動とか)されたらクリーンアップ関数でsetIntervalを止めることができます。
ちなみに、このコードのポイントは
setCount((prevState) => prevState + 1);
ここですね。
setCount(count + 1);
これでいんじゃね?
と思った人もいると思いますが(少なくとも私は思いました)、この書き方をするとcountが1のままで止まって上手く動きません。
これは、useStateの更新が非同期に行われるのが原因です。
setCount(count + 1)にしたコードと、上手く動かない理由は次の通りです。
const [ count, setCount ] = useState(0);
useEffect(
() => {
const time = setInterval( // 1秒ごとに定期実行する関数を、最初のレンダリングのときにセット。このときのcountは初期値の0
() => {
console.log("定期実行!");
setCount(count + 1);
// countに1を加えて、0+1=1。でも、この値はuseStateが非同期なので即座に(このsetIntervalの呼び出し中には)更新されない。
// だから、次のsetCountでは更新されてないcount=0が呼び出される。
}, 1000
);
return () => clearInterval(time);
},
[]
);
return (
<div className = {styles.main}>
<div>クマが{count}頭!どんどん増える!</div>
</div>
)
こういうときには、最初に書いた、
setCount((prevState) => prevState + 1);
この書き方をします。これは、前のstateを渡す更新関数、といわれるものです(prevStateの名前はなんでもOK)。
これなら、非同期の値更新を待たずに、前のstateを渡して次のstate更新ができます。
まとめ
useStateに続いて使用頻度の多いuseEffectの使い方と注意点について説明しました。
useStateとuseEffectの2つが使えればかなりの機能を実装することができます。
ただ、useEffectは実行タイミングとそれを制御するのにコツがいります。少しずつ理解していきましょう。
最近本格的にCSSの勉強を始めました。
WordPressでもGatsbyJSでも必要な基本的な知識「CSS」…これが奥深い。
もちろんCSSに関する基本的な知識はあるのですが、引き出しは多くしておきたいものです。
ワンランク上のホームページ、ブログ作成を目指して、特に「これはよかった」というオススメのCSS学習教材の紹介をしていきます。
動画編
Webデザインのオンラインスクールなどは沢山ありますが、数万円〜数十万円するので、なかなかハードルが高いですよね。
そこでオススメなのはUdemy。
このブログでもちょくちょく紹介させてもらっていますが、一つの講座買い切り、というのがありがたいです。
しかも、種類もかなり豊富!GatsbyJSの講座まである、というのは他ではなかなかないです。
そのなかでもオススメのCSSに関する講座を紹介しておきます。
CSSの講座とかは沢山あるので、「これよさそう!」と思ったものを探すのも楽しいかもしれません。
書籍編
CSSを実践的に書いていくなら、書籍も便利です。
本を片手にコーディング、というのは効率もいいですよね。
こちらもオススメの本を紹介しておきます。
CSSの基本ができている人が「次のステップに行きたいなぁ…」というときにオススメです。