ライフサイクルの話をする前に・・・
まず前提として下記のことが言えると思う。
Reactとはそもそも、状態の管理、更新を適切に描画に反映するためのライブラリである。さらに、Componentの最も大きな役割は描画(render)をすることである。
効率良く描画の表示、更新をするためにはComponentのライフサイクルを理解することが必須であると言える。
ライフサイクルとは
クラスコンポーネントを扱う際にライフサイクルは大きく分けて3つの期間がある。順番にMounting(マウント時)、Updating(更新時)、Unmounting(マウント解除時)。
公式でも下記のような図が紹介されているのでこちらを元に説明をする。
React lifecycle methods diagram
Mounting(マウント時)
Componentが表示されるまでの期間のこと。
- constructor()
- getDerivedStateFromProps() // 一般的ではないライフサイクル
- render() // ここで描画される
- componentDidMount()
componentDidMountで描画された後に何かしらの変更を加えたい、描画されてから出ないとできないことをここで行うことになる。ちなみに、最初にrenderされるときだけこの期間に入る。
例えばユーザー一覧を表示させるUIだとしたら、基本的なTableの見出しとユーザーの取得が完了するまでの間に表示させる Loading...
などがMounting時のrenderに相当する。そこまでの描画が完了したら componentDidMount
でユーザー一覧のAPIを叩く。
Updating(更新時)
Componentが表示され、Stateの更新を行う期間のこと。
- getDerivedStateFromProps() // 一般的ではないライフサイクル
- shouldComponentUpdate() // 一般的ではないライフサイクル
- render()
- getSnapshotBeforeUpdate() // 一般的ではないライフサイクル
- componentDidUpdate()
Stateの更新がされたら、1 ~ 5の順番でメソッドが呼ばれ再描画される。
先ほどの例でいけば、componentDidMount
でユーザー一覧のAPIを叩いた後に、取得が完了すればStateのusersにデータが入ることになる。
state = {
users: [
// ここの部分に入る
]
}
このusersが []
から [{id: 1, name: 'hogehoge'}]
と更新されたことを検知して再描画 > TableにUserを表示させることができる。
では、componentDidUpdate()
はどのタイミングで使うべきなのか例を挙げてみる。
ユーザーを新たに追加したいとしよう。UIでは一覧上で 新規登録
ボタンを押すとモーダルが出てきて ユーザー名を入力 > 登録とすることで追加することができるとする。
その場合、登録を押すことで APIを叩いてpostしているわけだが、送り終わったことがわかるStateの変更を検知して、ユーザー一覧の再取得をすることができる。
componentDidUpdate(prevProps) {
if(!prevProps.succeeded && this.props.succeeded) {
// ユーザー一覧を再取得
}
}
このときわかりにくいのが、prevPropsである。読んで字のごとく以前のPropsである。では、どういうことかというと componentDidUpdate
では更新される以前のPropsまたはStateと更新後のPropsまたはStateを比較することができる。それにより、特定のState(Props)が更新されたときにだけ何かしらの処理をすることができる。
Unmounting(マウント解除時)
他のComponentに移るときにだけ呼ばれる期間。
- componentWillUnmount()
現在のコンポーネントを破棄する直前に呼ばれるメソッド。
よく見落とされるのがアニメーションやタイマーを設定を ComponentDidMount
でしていた場合は componentWillUnmount
で破棄すること。そうしないと新しいコンポーネントのサイクルが始まった後も、その分のメモリが開放されないままになってしまう。
unsafeメソッドについて
実は以前は、他にもライフサイクルメソッドが存在していた。(今もしているけど非推奨)例えば、
- componentWillMount()
- componentWillUpdate()
- componentWillReceiveProps()
である。これらを自身のプロジェクトで見つけたらこっそり書き換えてあげると良い。
ちなみに、完全に個人的な見解だけど、結局ライフサイクルメソッドがたくさんあると、どこで何が変更されたなど、タイミングが難しくなってしまうことが多々ある。人に優しくないコードが簡単にできてしまうのではないかなと。
なので、基本的には componentDidMount
, componentDidUpdate
でまかなえるようにComponentの設計をすべきであると思う。
その流れが hooksのuseEffectに引き継がれているのかなと。
Functional Componentの場合のライフサイクル
以前までは クラスコンポーネントでないとライフサイクルを使うことができなかった。が、hooksの登場により Functional Component でもライフサイクルを使うことができるようになった。
useEffect
を使うことで簡単に 特定Stateの更新を検知することができる。
React.useEffect(() => {
// なんかやる
}, [// 検知したいState ])
例えば、エラーコードが検知したら何かやりたい場合は以下のような感じになる。
const [code, setErrCode] = React.useState<string>('')
React.useEffect(() => {
if (code) {
// codeが変更されたら何かやる
}
}, [code])
ライフサイクルメソッドの問題点?
状態の管理、更新を適切に描画をするのには必須と言えるライフサイクルメソッドだが、問題が発生するケースもある。
よくあるのが、componentDidUpdate内でifが乱立していて更新の検知が適切に行えているのかわかりにくいことがあげられる。
この問題の根本はライフサイクルメソッドが悪いのではなくComponentの設計に問題があることがほとんど。
Componentがやたらと大きくなりすぎていて多機能すぎていると言える。
次回はComponentの設計あたりを記事にする予定・・・