はじめに
アルプ株式会社でフロントエンドをしているmura-と申します。
弊社サービスのSaaSを管理するtoBむけプラットフォーム ScalebaseではReactを使ってフロントを構築しており、最近話題になっているhooksも導入しています。
昨年から発表はありましたが、今年の2月に、正式にhooksが利用できるようになったReact v16.8.0がリリースされました。
hooksはReact界にとってかなり良いインパクトを残し、hooksとはなにか、どう便利なのかという記事や話題をたくさん目にするようになったと思います。
hooksを導入することのメリット自体は調べればたくさんでてきますが、この記事ではReactの今まで提供されたコンポーネント変遷を交えて、最終的になぜhooksが導入されもてはやされるようになったかという視点で説明していきます。
TL;DR
忙しい方は 今までの問題点とhooksが解決すること をご覧ください
Reactの変遷
基本的なComponentの定義
hooksを駆使したものを除き、Componentの定義には createClass()
と React.Component
を継承したClassでの定義があります。
React.createClassを使った、React初期のComponentの定義
Class構文に対応している他の言語であれば、Component指向にしようとするとClassで表現するのを思いつきますが、当時Class構文に対応したES2015がまだ正式にリリースされてなかったのもあり、React.createClass()
でClass構文を使わずComponentを作成するメソッドが用意されていました。
See the Pen Use React Create Class by Kazuki Murahama (@mura-the-looper) on CodePen.
createClassによる定義でも今と同じようにライフサイクルメソッドやStateの初期値、渡すPropsなどを引数のオブジェクトに定義する必要がありました。
当時はVisual Studio CodeがなくTypeScriptもサポートされてなかったですし、普通のエディタでカジュアルに触ると、長大なオブジェクトの定義でカンマのつけ忘れなどでsyntax errorを起こしてしまうも多くあったことを覚えています。
React.Componentを継承したClassBasedなコンポーネントの定義
Reactは、v0.13から正式にES2015のClass構文に対応しました。
React.ComponentをextendsしてClassを定義すると、そのClassのインスタンスメソッドとしてライフサイクルメソッドを定義できます。createClassと同じく各インスタンスは render()
が必ず実行されます。
See the Pen Use React Class based component by Kazuki Murahama (@mura-the-looper) on CodePen.
Classですので、constructorになにか処理をさせたり、Stateをインスタンス変数として定義できます。
定義において基本的には、React.createClassでやっていたことがClassで書けるようになったことにより、よりComponentらしく書けるようになりました。
※ライフサイクルメソッドはv16.3から大きく変更しています。
参考: React v16.3 changes
パフォーマンスを考慮したコンポーネントの定義
React.Component
だけだとシンプルに書きたい場合やパフォーマンスを考慮したときに難点がでてきました。それを解消するためのStateless Functional Componentと React.PureComponent
が用意されました。
関数で定義するStateless functional componentの定義の仕方
Classを使って書けるのは開発者としては嬉しいですが、Stateすら持たず、ただ render()
だけしたいだけのComponentなど、通常のClassによる定義がオーバースペックになる場合がありました。
そこでシンプルでカジュアルに定義できるStateless functional component (SFC)がv0.14で用意されました。
See the Pen StatelessFunctinalComponent by Kazuki Murahama (@mura-the-looper) on CodePen.
JSXをreturnする関数を書けばいいだけなのでだいぶ見通しもいいです。メモリを確保する必要がないことと、React自体がSFCのパフォーマンスを最適化させているのでパフォーマンス的にも向上がはかれます。( 45%速くなった事例もあるようです。)
ただし、個人的には大きなデメリットがあり、SFCを定義したあとでStateをもたせたくなった場合、通常のClassに書き換えざるを得ず大変でした。だったら、最初から通常のComponentを定義しよう、と思っている人もいたのではないかと思います。
React.PureComponentを使ったコンポーネントの定義
Reactでは親のコンポーネントのStateが変わったときなど、その下に含まれる子コンポーネントまで再描画されます。仮に子コンポーネントに渡しているPropsに差分がなくても、再描画してしまうので、子コンポーネントで難しい計算などをしていた場合パフォーマンスに影響がでたりします。shouldComponentUpdate() を使うことでコンポーネントを再描画しないようフラグをたてることができますが、毎回それを書くのは手間ですし忘れます。
v15.3でリリースされた React.PureComponent
は、基本的には React.Component
と変わらないのですが、自動的に shouldComponentUpdate()
を実行して必要以上に再描画しないよう、取り計らってくれます。(ただし、Shallowな比較なのでObjectやArrayのPropsを渡している場合は注意が必要です)
hooksが登場するまでに使われていたComponentと用途のまとめ
諸々経緯があり、いろんなComponentが用意されていますが、hooksが登場するv16.8以前は、通常は下記のような用途で使われていました
Componentの種類 | 用途 | 備考 |
---|---|---|
React.createClass()による定義 | ES2015などモダンな環境でない場合に使われる。そうでなければ通常使わない。 | 最近はReactDOMのようにReact本体からも切り離されている |
React.Componentを継承したClass | Stateをもったり、親Componentとして使われる。通常使われるComponent。 | |
Stateless Functional Component (関数Component) | Stateを持たない、難しいことをさせたりしないテンプレートのような用途で使われる。 | |
React.PureComponentを継承したClass | Stateをもち、子コンポーネントなどで複雑な計算などをする場合使われる。 | v16.6で追加された React.memo() を使えば, Stateさえ持たなければSFCでも役割を担うことができるようになった。もちろん通常のComponentで shouldComponentUpdate() を定義すれば同等なことができるのでプロジェクト次第では使われてないかもしれません。 |
Componentに共通する振る舞いを与える
ここまでComponent自体について紹介しましたが、定義したComponentに対して、ロジックが一元化された任意の振る舞いやデータを与えたいケースもあります。Componentに任意の振る舞いを与えるために、どうしてきたかということも紹介します。
React.createClass() にのみ存在したMixin
Mixinはかなり便利な機能でした。reactのライフサイクルメソッドとして、componentDidMount
や componentDidUpdate
などがありますが、任意のComponentに好きに振る舞いを注入させることができました。
Higher-order Componentを使って振る舞いを与える
ClassBasedなReactではMixinがサポートされなくなりました。Mixinは問題があり ますし、公式でもHigher−order Componentを使えとアナウンスされてました。
Higher-order Component(HoC)自体はReact自体の機能ではなく、あるComponentにPropsや機能などを渡して返すためのイディオムです。Redux Reactの connect() など、ライブラリが作るデータや機能を、Componentに渡すときなどによく使われる手法です。
// react reduxのconnectを使ってReduxのStateやPropsを渡す
connect()(MyComponent)
connect(mapState)(MyComponent)
connect(
mapState,
null,
mergeProps,
options
)(MyComponent)
See the Pen abzZQrg by Kazuki Murahama (@mura-the-looper) on CodePen.
HoCに関する詳細は下記をごらんください。
高階(Higher-Order)コンポーネント
render propを使って振る舞いを与える
Reactが提供する render prop というものを使って振る舞いを与えることもできます。
render propとHoC、好きな方法を使うことができますが、HoCは名前衝突したり記述が煩雑になったりしますし、型の定義が大変ですのでrender propの方が書きやすいとは思います。
See the Pen RenderPropsComponent by Kazuki Murahama (@mura-the-looper) on CodePen.
hooksを使ったComponentの定義と、今までの問題点とhooksが解決すること
ここまで、長くなってしまいましたが今まで紹介したことを踏まえて、hooksを比較してみます。
hooksで定義したComponent
hooksを使うと、下記のようにComponentが定義できます。
See the Pen React Functional Component by Kazuki Murahama (@mura-the-looper) on CodePen.
まず、大きな点として、React.Componentを使ってClassとして定義しなくて良くなった点が大きいです。
Stateless functional component(SFC)について先程紹介しましたが、Functional Componentなのに状態を持つことができるようになった、と言うことができます。ここでは仕組みについて言及はしませんが、reactは useState
などのAPIを提供したおかげで、関数コンポーネントでStateを扱うことができるようになりました。
また、今まで componentDidMount
などで扱っていたであろう、コンポーネントに対する副作用も useEffect
などを使って表現できます。
ただそれだけ、といえばそれだけなのですが、今までのReactの問題点を考えるとかなりインパクトがあることがわかります。
今までの問題点とhooksが解決すること
- 今までのReactの問題点
- どのコンポーネントを使っていくか都度考えないといけない。
- SFCを定義したとしてもあとからStateをもたせたくなってClassに書き換える手間がある。
- PureComponentを使ったとしても結局最適化できない場合があり
shouldComponentUpdate()
を定義する手間が発生する。など
- ライフサイクルメソッドの複雑・煩雑さ
- 一番最初に動作するライフサイクルメソッドはどれか、どのライフサイクルメソッドで副作用がおきるか、関連するロジックであってもライフサイクルごとに定義しないといけないので、いろんなロジックが混ざるとかなり複雑になりバグを生む。
- Wrapper Hell
- HoC, render propなどを駆使したとて結局、Componentのネストが深くなってしまい可読性がおちる
- どのコンポーネントを使っていくか都度考えないといけない。
※上記は主観もふくまれますので hooksが実装された動機もごらんください
- hooksが解決すること
- Componentは基本的にすべてFunctional Componentを使って定義してよい
- ライフサイクルメソッドから開放され、ロジックはreturnするJSXの前にすべて表現できる
そのおかげで下記のメリットがある- コードは上から下に実行され、宣言的に書ける
- ロジック部分を明確にわけることができ、一元的に扱える
- 独自のhooks (custom hooks)も定義できる
- 状態を直接扱えるため、HoCやrender propを使う必要がなく、Wrapper hellを避けることができる
- ライブラリもhooksに対応していればそのライブラリが提供する値や関数を直接扱える
先に紹介したReact Reduxも hooksを提供しており connect()
を使う必要がなく下記のように扱えます。
import React from 'react'
import { useSelector } from 'react-redux'
export const CounterComponent = () => {
const counter = useSelector(state => state.counter)
return <div>{counter}</div>
}
さらなるhooksについての概要はこちらをごらんください
フック早わかり
hooksの問題点
銀の弾丸は存在しませんが、やはりhooksも同様です。下記のような問題点があります
- Stateが変更されたとき、従来と同じくComponentは再描画される。上から下に実行されるがゆえに、値や関数を必要に応じてメモ化する必要がある。メモ化をうまく扱えないと無限ループを起こしてしまう可能性もある。
- hooksは制限があり、いつでもどこでも動かせるわけではない。例えばif文のなかで
useEffect()
は実行できない。 - メモ化などを含め、新たな考え方が必要になるため、hooksは難しいと感じるケースもある
注意
Context APIなどここでは紹介しきれてないこともたくさんありますがご容赦ください。
おわりに
個人的にも今までComponentの設計やHoC、render prop、Wrapper Hellなど様々なことに悩まされてきましたが、hooksを使うことでかなり見通しのよいComponentを定義できるようになったと実感しています。すべてのライブラリがhooksを提供するわけではありませんが、よく使われている主要なライブラリもhooksを提供しはじめてます。hooksを使うにはReactのバージョンアップしないといけないなどの手間もあると思いますが、それを差し引いてもメリットが大きいのでまだ導入してない方はぜひ使ってみてください。
なにか間違ってる箇所とかありましたらツッコミいただけますと幸いです!※特にサンプルコードは作り込む時間ががが
弊社のサービスScalebaseでも、絶賛hooksに書き換え中です。興味ある方、お手伝いいただける方はぜひご連絡ください!技術に関するお話だけでも大歓迎です。
twitter: https://twitter.com/mura_cx