こんにちは!
この記事は「レガシー」を保守したり、刷新したりするにあたり得られた知見・ノウハウ・苦労話 by Works Human Intelligence Advent Calendar 2022 の 5日目の内容として投稿させていただきます。
今回は、「Reactのクラスコンポーネントを関数コンポーネントへリファクタリングする際の注意点(useState, useEffect編)」について書きたいと思います!
この記事の内容は実際に私がクラスコンポーネントから関数コンポーネントへのリファクタリングを行った際にハマったポイントをご紹介していきます。補足などあればコメントいただけると大変うれしいです!
今回、ハマったポイントすべてを書ききることはできませんでしたが、別のハマりポイントはまた今度ご紹介できればと思います。
クラスコンポーネントと関数コンポーネントって?
詳しくは下記を御覧ください
https://ja.reactjs.org/docs/react-component.html
クラスコンポーネントはクラスベースで定義され、関数コンポーネントは関数ベースで定義されます。
そもそも何でクラスコンポーネントを関数コンポーネントに書き換えるのか?
様々な理由があるかと思いますが、私が思う理由は大きく分けて下記3点です。
- 記述がシンプルになる
a. thisやconstractor、メソッドのbindなどを書く必要がなくなります - stateを用いた関数を共通化できる
a. カスタムフックを使用すれば、stateを書き換える様な関数を共通化できます。 - 最近のライブラリは関数コンポーネント前提で作られていることが多い
クラスコンポーネントを関数コンポーネントへ書き換える際に気をつけるべきポイント
さて、ここからが本題です!
私がハマって思った「クラスコンポーネントを関数コンポーネントへ書き換える際の注意点」を、state hookとライフサイクルメソッドに絞ってご紹介していきます!
stateの書き換え(useState)
クラスコンポーネントは下記のようにstateの初期値を定義します。
class Hoge extends React.Component<HogeProps, HogeState> {
constructor(props: HogeProps) {
this.state = {
title: 'タイトル',
name: '名前',
}
}
//省略
}
そして、下記のようにstateを更新します。
this.setState({
title: 'タイトル2',
name: '名前2'
})
関数コンポーネントではstateの制御はstate hookを用います。
https://ja.reactjs.org/docs/hooks-state.html
なのでこれを素直に関数コンポーネントに直すと下記のようになります。
const Hoge () => {
const [hogeState, setHogeState] = useState<HogeState>({
title: 'タイトル',
name: '名前'
})
// 省略
}
オブジェクトとしてtitleとnameを持つ形になりますね。
別にこれでも良いのですが、この書き方だとTypeScriptを使用している場合に下記の様な問題が発生する場合があります。
- stateを更新する際に、すべてのプロパティを更新しないと型エラーが起きる
- それぞれのプロパティを別の子コンポーネントに渡している状態で、1.を考慮してstate更新の際はすべてのプロパティを同時に行おうとすると、メモ化していても余計なレンダリングが走ってしまう
余計なレンダリングはどうしても避けたいですし、必要がないstateを更新しないといけないというのは結構違和感ありますよね...
これらを解決するには、下記の様にstateを分ければいいのかなと思っています。
const Hoge () => {
const [title, setTitle] = useState<string>('タイトル')
const [name, setName] = useState<string>('名前')
// 省略
}
上記のような問題が発生しうることを考えると、同時に変更し得ないstateは基本的に分離してしまったほうが良いかなと思っています。
ライフサイクルメソッドの書き換え(useEffect)
よく使われるライフサイクルメソッドは下記の3つかなと思います。
- componentDidMount
- componentWillUnmount
- componentDidUpdate
これらは、関数コンポーネントではuseEffectで表現できます。
componentDidMount
componentDidMountはマウント時に処理が走ります。一番単純に置き換えできます。
componentDidMount() は、コンポーネントがマウントされた(ツリーに挿入された)直後に呼び出されます。DOM ノードを必要とする初期化はここで行われるべきです。リモートエンドポイントからデータをロードする必要がある場合、これはネットワークリクエストを送信するのに適した場所です。
componentDidMount() {
this.state({
name: '名前',
title: 'タイトル'
})
}
↓
useEffect(() => {
setTitle('タイトル')
setName('名前')
}, [])
componentDidMount内の処理が複雑で、上記のように書き換えることはできないけど「マウント時に一度だけ処理を走らせたい!」という場合もありますよね。そういう場合は下記のようにstateを使って制御すれば良いかなと思っています。
useEffect(() => {
if (hasLoaded) return
// 複雑な処理
setHasLoaded(true)
}, [hasLoaded])
componentWillUnmount
componentWillUnmountは下記のようにuseEffectのreturn文の中に処理を書くことで表現できます。
componentWillUnmount() は、コンポーネントがアンマウントされて破棄される直前に呼び出されます。タイマーの無効化、ネットワークリクエストのキャンセル、componentDidMount() で作成された購読の解除など、このメソッドで必要なクリーンアップを実行します。
componentWillUnmount() {
this.cleanUp()
}
↓
useEffect(() => {
return () => {
cleanUp()
}
},[])
componentDidUpdate
componentDidUpdateは少し曲者です。理由は、多くの場合更新前のpropsやstateの値を参照し、現在のpropsやstateと比較するからです。
更新が行われた直後に componentDidUpdate() が呼び出されます。このメソッドは最初のレンダーでは呼び出されません。
コンポーネントが更新されたときに DOM を操作する機会にこれを使用してください。現在の props と前の props を比較している限り、これはネットワークリクエストを行うのにも適した場所です(たとえば、props が変更されていない場合、ネットワークリクエストは必要ないかもしれません)。
componentDidUpdate(prevProps: HogeProps, prevState: HogeProps) {
if (prevProps.id !== this.props.id && prevState.title !== this.state.title ) {
this.doSomething()
}
}
↓以前のpropsやstateの比較をせず、ただ単に指定のpropsやstateが変化した時に発火させたい場合
useEffect(() => {
doSomething
}, [title, props.id])
// 依存配列で指定されている値が変化した際に発火
上記とは違い、以前のpropsやstateの値を比較して処理を変える場合、対策を講じないとuseEffectに書き換えることができないです。理由は、useEffectはcomponentDidUpdateのように更新前のstateやpropsを受け取る方法がないからです。本来は、以前のpropsやstateを比較しないように、propsやstateを更新する関数などで制御したいところです。しかし、リファクタリングしているとその状態を作り出すことが難しかったり、面倒だったりしますよね。。。
どうしても以前のstateやpropsを参照したい場合は、公式ドキュメントに書いてあるとおり下記のようなカスタムフックを作って使用してあげれば良さそうです。
import { useEffect, useRef } from 'react';
export const usePrevious = <T>(value: T) => {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
};
const prevTitle = usePrevious(title)
const prevId = usePrevious(props.id)
useEffect(() => {
if (prevId !== props.id && prevTitle !== title ) {
doSomething()
}
}, [title, props.id, prevTitle, prevId])
あとがき
最後までご覧いただきありがとうございました!
まだまだハマった点はあるのですが、書ききれなかったのでまた今度書きたいなと思います。