Effectとは?
React Hooksの一種で副作用を実行するための処理
- APIからのデータ取得
- DOMの更新
構文
副作用は useEffect()
によって定義される。
第一引数
副作用を実行するコールバック関数(引数なし)を指定する。
useEffect(() => {
console.log('レンダリングごとに実行');
});
クリーンアップ関数
第一引数の関数はクリーンアップ関数を返すことが可能。
クリーンアップ関数は 「コンポーネントのアンマウント時」 「コンポーネントの再レンダリング時」に実行 される
useEffect(() => {
console.log('レンダリングごとに実行');
return () => {
console.log('アンマウント時に実行');
};
});
第二引数
第二引数には 「依存配列」 を指定することができる。
依存配列
副作用関数が依存する値を配列形式で指定する。
「配列内の値が変更されるたびに、指定した副作用が再実行」 される。
依存配列を省略した場合
依存配列を省略すると、第一引数の関数は 「コンポーネントがレンダリングされるたびに実行」 される。
useEffect(() => {
console.log('レンダリングごとに実行');
});
依存配列に[]
を指定した場合
依存配列に空の配列 []
を指定すると、副作用は 「コンポーネントのマウント時に一度だけ実行」 され、アンマウント時にクリーンアップ関数が実行されます。
useEffect(() => {
console.log('マウント時に一度だけ実行');
return () => {
console.log('アンマウント時に実行');
};
}, []);
依存配列に値を指定した場合
userId
の値が変わるたびに fetchUserProfile
関数が呼ばれ、新しいユーザープロファイルを取得する
useEffect(() => {
fetchUserProfile(userId).then(data => {
setUserProfile(data);
});
}, [userId]);
依存配列のWarning
以下のコードはtimeLeft
とmaxCount
を副作用の中で使用しているが、これらが依存配列に含まれていないためにwarning
が発生する。
useEffect(() => {
if (timeLeft === 0) setCount(maxCount);
});
発生理由
この副作用は timeLeft
の値を確認、 → maxCount
を利用して値を更新しているが、
timeLeft
及び、 maxCount
の値に変更がない場合、 副作用の実行に意味がないため
正しい実装
useEffect(() => {
if (timeLeft === 0) setTimeLeft(maxCount);
}, [timeLeft, maxCount]);
このようにすることで関連する副作用が適切に実行される。
useEffectは複数定義可能
useEffect
はコンポーネント内で複数回定義することが可能。
これにより、異なる副作用を分けて管理できる。
useEffect(() => {
console.log('レンダリングごとに実行');
}, []);
useEffect(() => {
console.log('レンダリング');
});
サンプルコード
ここまでの説明を踏まえたタイマーコンポーネントの実装です。
import { FC, useEffect, useState } from 'react';
type TimerProps = {
initialCount?: number;
};
const DEFAULT_COUNT = 60;
const Timer: FC<TimerProps> = ({ initialCount = DEFAULT_COUNT }) => {
const [secondsLeft, setSecondsLeft] = useState(initialCount);
const tick = (): void => setSecondsLeft((current) => current - 1);
const resetTimer = (): void => setSecondsLeft(initialCount);
useEffect(() => {
if (secondsLeft === 0) setSecondsLeft(initialCount);
}, [secondsLeft, initialCount]);
useEffect(() => {
const timerID = setInterval(() => {
tick();
}, 1000);
return () => clearInterval(timerID);
}, []);
return (
<div>
<div>
<div>Count</div>
<div>{secondsLeft}</div>
</div>
<button onClick={resetTimer}>
Reset
</button>
</div>
);
};
export default Timer;
[参考] クラスコンポーネント
昔のクラスベースのReactコンポーネントで記載された場合
import React, { Component } from 'react';
type TimerProps = {
initialCount?: number;
};
type TimerState = {
secondsLeft: number;
};
const DEFAULT_COUNT = 60;
class Timer extends Component<TimerProps, TimerState> {
timerID?: NodeJS.Timeout;
static defaultProps = {
initialCount: DEFAULT_COUNT,
};
constructor(props: TimerProps) {
super(props);
this.state = {
secondsLeft: props.initialCount || DEFAULT_COUNT
};
}
componentDidMount() {
this.startTimer();
}
componentDidUpdate(prevProps: TimerProps, prevState: TimerState) {
if (this.state.secondsLeft === 0) {
this.resetTimer();
}
}
componentWillUnmount() {
this.stopTimer();
}
startTimer = () => {
this.timerID = setInterval(() => {
this.tick();
}, 1000);
}
stopTimer = () => {
if (this.timerID) {
clearInterval(this.timerID);
}
}
tick = () => {
this.setState((prevState) => ({
secondsLeft: prevState.secondsLeft - 1
}));
}
resetTimer = () => {
this.setState({
secondsLeft: this.props.initialCount || DEFAULT_COUNT
});
}
render() {
return (
<div>
<div>
<div>Count</div>
<div>{this.state.secondsLeft}</div>
</div>
<button onClick={this.resetTimer}>
Reset
</button>
</div>
);
}
}
export default Timer;
まとめ
副作用をコンポーネントから分離して定義できるので読みやすいですね。