はじめに
React 公式ドキュメントを読み、useEffect を使うべきタイミング、逆に使うべきではないタイミングを少し理解できたような気がしたため、自分なりの解釈を記事にまとめてみました。
useEffect とは
useEffectは関数(Functional)コンポーネントで利用することができる React フック。
データフェッチ、React の状態の外にあるデータをサブスクライブするとき、外部システムを同期させたい時などに使用します。
使い方
useEffect を使うときはコンポーネントのトップレベルで呼び出します。
useEffect(エフェクトのロジックを記述した関数, 依存する変数の配列(省略可))
第一引数にエフェクトのロジックを記述した関数を記述します。
必要であればクリーンアップ関数を返すようにします。
クリーンアップ関数を渡すと、再レンダーが起きたとき、
古い値を使ってクリーンアップ関数を実行 -> 次に新しい値を使ってセットアップ関数を実行する、といった挙動になります。
コンポーネントが DOM から削除された後にもクリーンアップ関数は実行されます。
↓具体的な使い方イメージとしては以下のような感じです。
function Sample() {
const [isOpen, setIsOpen] = useState(false);
useEffect(() => {
const timeoutId = setTimeout(() => {
setIsOpen(true)
}, 5000)
// クリーンアップ関数
return () => {
clearTimeout(timeoutId)
}
}, [])
// ...
}
(本題)useEffect を使うべきではないタイミングを考えてみる
簡単に使い方を説明したところで、では、useEffect は具体的にどんな時に使うのか?を考えてみます。
公式の文章を引用すると、以下のように書かれていました。
外部システムと同期する必要がない場合、エフェクトはおそらく不要です。
「外部システムが関わっているか」が useEffect を使う/使わない、の一つの判断指標になりそうです。
useEffect が不必要である具体例
具体的にどのような場面で useEffect が不必要なのかが公式に記載されていました。
例として、以下のような場面では必要ないようです。
- props や state に基づいて state を更新したいとき
- props の変更に合わせて state を更新したいとき
- ユーザーのアクションにより何かを実行したいとき
- 重たい計算をキャッシュしたいとき
それぞれもう少し詳しくみていきます。
props や state に基づいて state を更新したいとき
良くない例
function Form() {
const [firstName, setFirstName] = useState('佐藤');
const [lastName, setLastName] = useState('花子');
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
// ...
}
上記の例を見ると、firstName
と lastName
を組み合わせ、fullName
を更新しています。
useEffect の中で state を更新しているため、「最初のレンダリングが実行される -> レンダリング後に useEffect が実行される -> setFullName
で state が更新されているため、再度レンダリングされる」と、無駄に再レンダリングが発生しています。
また、fullName は state に入れなくても計算できる値であるため、state 必要ありません。
良い例
function Form() {
const [firstName, setFirstName] = useState('佐藤');
const [lastName, setLastName] = useState('花子');
const fullName = firstName + ' ' + lastName;
// ...
}
props の変更に合わせて state を更新したいとき
この場合も「props や state に基づいて state を更新したいとき」と同様、useEffect の中で state を変更することで不要な再レンダリングがおこるため、useEffect を使わず、レンダー中で直接 state を変更するようにします。
良くない例
function Cart({ items }) {
const [itemList, setItemList] = useState(null);
useEffect(() => {
setItemList(null);
}, [items]);
// ...
}
前の items を state として保持し、渡される props( items )と比べることで変更があったかを検知できるようにします。
そうすることで、useEffect を使わなくても実装が可能になります。
良い例
function Cart({ items }) {
const [itemList, setItemList] = useState(null);
const [prevItems, setPrevItems] = useState(items);
if (items !== prevItems) {
setPrevItems(items);
setItemList(null);
}
// ...
}
ユーザーのアクションにより何かを実行したいとき
例えば「ユーザーがボタンをクリックしたときに POST リクエストを送りたい」などといった場合、useEffect を使う必要はありません。
コンポーネントがユーザに表示されたために実行されるべきなのか、何かのアクションをきっかけに実行されるべきなのかを考えます。
何かのアクションをきっかけに実行される場合はイベントハンドラの中に処理を書くべきです。
「ユーザーアクション起点のものはイベントハンドラに書くでしょ」と思うのではないかと思いますが、Bad な例として、各イベントハンドラで共通の処理があった場合に、それを共通化するために useEffect を使用するというものがあります。
良くない例
function ItemPage({ item, addToItemList }) {
useEffect(() => {
showNotification(`${item.name}を追加しました`);
}, [item]);
function handleAddButtonClick() {
addToItemList(item);
}
function handleCheckoutClick() {
addToItemList(item);
navigateTo('/checkout');
}
// ...
}
handleAddButtonClick()
/ handleCheckoutClick()
のどちらでも同様の処理をしたいとき、それぞれで showNotification()
を呼び出すのは繰り返しに感じるため、useEffect を使って共通化しています。
これには問題があり、ページを更新するたびにshowNotification()
が実行されるため、期待する挙動にはなりません。
共通化として useEffect を使うのではなく、関数を定義し、その関数をそれぞれのイベントハンドラから呼び出すようにします。
良い例
function ProductPage({ item, addToItemList }) {
function buyItem() {
addToItemList(item);
showNotification(`${item.name}を追加しました`);
}
function handleAddButtonClick() {
buyItem();
}
function handleCheckoutClick() {
buyItem();
navigateTo('/checkout');
}
// ...
}
重たい計算をキャッシュしたいとき
重たい計算( heavyCalculation()
)をレンダリング毎に何度も計算したくないため、計算に必要な値が更新された時のみ再計算する手段として useEffect を使用しています。
useEffect の使い時をなんとなく理解するまでは、割とこれを考えてしまいがちだった気がする (レンダリング毎に計算する必要がないものは値が変わったときだけ計算するように useEffect に入れるのが良いのかな?と思っていた)
function TodoList({ items, filter }) {
const [calculateItems, setCalculateItems] = useState([]);
useEffect(() => {
setCalculateItems(heavyCalculation(items, filter));
}, [items, filter]);
// ...
}
こちらも useEffect の中で state を更新しているため、不要な再レンダリングが起こります。
重たい計算をキャッシュするには useMemo
を使うのが最適です。
function TodoList({ items, filter }) {
const calculateItems = useMemo(() => {
return getFilteredTodos(items, filter);
}, [items, filter]);
// ...
}
さいごに
useEffect を使うべきタイミングの理解が曖昧でしたが、改めて React 公式を読み返してみて、理解が深まり良かったです。
React を勉強したてで同じように感じている方がいらっしゃったら、参考に貼ってある公式リンクをぜひ読んでみてください。
参考