Reactは便利なライブラリですが、ライブラリの想定を外れる使い方をするとうまく動かないとか、進化が続いているのですでに古くなってしまった書き方とかが存在します。
ここでは、そのような、Reactでやるべきでない書き方についてまとめてみました。
★★★★
この「★★★★」は、「守らないと正常動作しなくなる」レベルのものについて扱います。
JSXを使うファイルでReactを用意し忘れる
JSXはReact.createElementに変換されますので、直接アクセスしているように見えなくても、名前空間内にReactが必要です。
コンポーネント内部からpropを書き換える
あくまでpropsは親から子へ渡されるものなので、子の側で書き換えることはReactの枠組みの想定外となります。
setState以外の手段でstateを書き換える
クラスコンポーネントのコンストラクタは別ですが、それ以外の場所でstateを書き換えたい場合はthis.setStateを使いましょう。直接書き換えると、後のsetStateで別な値が上書きされるなど、正常でない動作の原因となります。
React外の手段で、Reactが生成したDOMを書き換える
ReactのDOMは差分更新を行いますので、React外で書き換えると差分更新が破綻してしまいます。
メモ化したコンポーネントで、propやstate内のオブジェクトを破壊的に変更する
PureComponentやReact.memoは、破壊的変更がないことを前提として処理を効率化するものですので、破壊的変更を行うものに使うと前提が崩壊します。破壊的変更が避けられないのであれば、PureComponentなどを使わないようにしましょう。
keyの重複
コンポーネントの配列で、各要素を識別するためにkeyを入れますが、key同士が重複すると正常に動作しません。
HTMLのつもりで属性を書く
idやnameなどはJSXでもHTMLでも同じですが、JSXはJavaScriptということもあって、JavaScriptから見えるpropertyとしての名前が必要となります。たとえば、classではなくclassName、readonlyではなくreadOnlyなどです。
なお、カスタムデータ属性のdata-***や、アクセシビリティのaria-***属性(React公式)は、そのままハイフン区切りで書きます。
valueのデフォルト値をundefinedのまま放置する
Reactの入力コンポーネントにはControlledとUncontrolledがありますが、これはvalue == nullかで判定されます1。ということで、オブジェクトの存在しない値を参照してundefinedとなっているものをそのままvalueに供給してしまうと、Uncontrolled扱いとなってしまいます。あとからControlledに切り替われば警告を発します。
もちろん値をきっちり管理する必要もあるのですが、文字列が入るところならvalue={this.state.someValue || ''}のように強制的に空文字列に変換してクリアする方法も考えられます。
render関数を、何も返さずにreturnする
render関数(あるいは関数コンポーネント)の返り値として、JSXやその配列以外にnullも許容されていますが、undefinedは適切ではありません。何も描画したくないときは、return;ではなくreturn null;で脱出してください。また、うっかりreturnを書き忘れないように注意も必要です。
Hooksの掟を守らない
これはReact公式や自分の過去記事にもありますが、Hooksは「呼ばれる順番」でデータを紐づけています。そのため、「条件分岐」「ループ」「途中脱出」などを入れず、毎回同じ順序で同じ回数だけのHookを呼ぶ必要があります。
なお、これはeslint-plugin-react-hooksが警告してくれます。
Hooksのdeps配列に必要なものを渡さない
useCallbackやuseMemoには依存する変数の配列を渡す必要がありますが、必要な変数を入れ忘れると、クロージャが値を束縛したままになるので、適切な値が入らないことになります。自分自身、state値を入れ忘れたことがあります。
なお、これもeslint-plugin-react-hooksが警告してくれます。
★★★
この「★★★」は、「一応動作はするけど、できるかぎり避けたほうがいい」レベルのものを挙げていきます。
ReactDOM.renderの返り値を使う
いまのところ、ReactDOM.renderはコンポーネントへの参照を返しますが、この値を使うのは非推奨です。コンポーネントインスタンスが必要な場合、refを利用しましょう。
findDOMNodeの利用
仮想DOMにrefを付けることで代用できますし、関数コンポーネントに対しては利用できません。findDOMNodeを使わないようにしましょう。
古いライフサイクルメソッドの使用
React 16.3以降、componentWillMount、componentWillUpdate、componentWillReceivePropsといったライフサイクルコールバックが、「将来の非同期化に対応できない」ということで非推奨となりました。
React.Componentの継承
Reactチームによれば、コンポーネントの再利用はコンポジションによってなされるべきであって、FacebookでReactを使っていても継承が必要な場面は見つからなかった、とのことです(公式)。
forceUpdate()の利用
クラスコンポーネントには、forceUpdate()というメソッドがありますが、そもそもこれが必要になる例というのが、
-
propsやstate以外で、外部で変化する値を参照していた場合 -
propsやstateの一部を破壊的に変更した結果、更新が見えなくなった場合 -
shouldComponentUpdateで必要な更新まで弾いてしまう場合
など、コンポーネントの設計として良くないパターンが大半です。きちんと作れば、必要になることはまずありません。
keyに配列のインデックスを使う(あるいは、key自体を与えない)
全く書き換わらない配列からmapして生成する場合など問題のない場面もありますが、多くの場合配列のインデックスと中身の関係性は実行中に変化しうるものです。インデックスをキーとすると、中身の変化についていけない場面が考えられます。
this.setState内でthis.stateを参照する
現在の値をthis.stateから参照すると、複数のsetStateが重なった際に適切でない値となる危険があります。setStateを関数形式にすればこの問題は完全に回避できるので、そう書きましょう。
★★
この「★★」は、「動くけどパフォーマンスに悪影響するもの」あるいは「問題はそこまで大きくないけど、容易に回避できるもの」を取り上げていきます。
JSXに即時関数を直書きする
onClick={() => someFunction()}のようにJSXに与える値の内部で即時関数を作ってしまうと、実行のたびに関数インスタンスが違ってくるため、メモ化が意味をなさなくなってしまいます。クラスコンポーネントであればプロパティに、関数コンポーネントであればuseCallbackなどで関数インスタンスを固定しましょう。
※ refはReact内部で処理され、子コンポーネントに渡されないため、Callback Refでも問題ありません。
JSXにスタイルを直書きする
JSXではstyleはオブジェクト形式で与えることとなります。そして、上の関数のときと同様、オブジェクトは毎回インスタンスが変化します。ということで、「固定のスタイルを書いたら、そのスタイルオブジェクトが毎回変化してメモ化が効かなくなる」という愉快なことになってしまいます。
そもそも論を言えば、変化しないスタイルはCSSで与えるべきでしょう。
JSXのpropsに固定的なJSXを直書きする
これも上2つと同様に「同じでいいオブジェクトが変化し続ける」事態とはなりますが、これは@babel/plugin-transform-react-constant-elementsを使うことで、記法を変えずに解決可能です(自分の過去記事)。
useMemoやuseCallbackを1引数で呼ぶ
これらのhooksをdepsなしで呼び出した場合、メモ化は何も行われず、引数がそのまま返ってきます。メモ化が不要なら、useMemoやuseCallbackを使わない形で問題ないでしょう。
(useEffectを同様な形で呼んだ場合、「毎回実行する」という意味を持つものとなります)
useMemoやuseCallbackのdepsに、その場で生成するオブジェクトを入れる
depsの配列はObject.is相当で要素ごとに比較されますので、同じものを再生成できるプリミティブや、実行を超えて引き継がれるオブジェクト(別にuseMemo、prop、state、外部変数など)ならいいのですが、その場で生成したオブジェクトを入れるとメモ化が無効となります。
childrenを取るコンポーネントをメモ化する
これは自分が以前に書きましたが、childrenの内部構造の関係もあって、メモ化が有効な例がごく限られてしまいます。
デバッグ時にdisplayNameをセットしない
コンポーネントにdisplayNameがあるのとないのとで、デバッグ時のやりやすさが格段に違ってきます。
なお、関数宣言(function Component())やクラスの場合、名前がdisplayNameとして自動で使われますが、圧縮すると吹き飛びますのでその点は要注意です。
★
この「★」は、「ある程度考えておいたほうがいいけど、そう簡単に実装できない場合もある」ものを中心に触れていきます。
componentDid***でsetStateする
もちろん必要な場面もあるかもしれませんが、描画完了時のコールバックで改めてsetStateを行うと、そこから再度動き出してしまいます。できれば避けたい書き方です。
副作用のあるconstructor()
これも自分が以前に書きましたが、React 17の非同期レンダリングでは、コンストラクタも1コンポーネントに対して複数回実行されうるものとなります。外部になにか影響するものはcomponentDidMountに移動しましょう。
関数コンポーネントで、他のstateを参照してのsetState
関数コンポーネントで、複数のstateが絡む更新を行いたい場合、あまりよくないのですがstateの値を直接参照することとなってしまいます。できることなら、関連する複数のstateを1つのオブジェクトにまとめて、更新関数やreducerなどで一括処理させるようにするほうがいいでしょう。
-
valueに、プリミティブでもないdocument.allなんてものを与える人は無視することにします。 ↩