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
なんてものを与える人は無視することにします。 ↩