会社の勉強会で「Vue.jsでJSXを書く」という内容で話をしてきました。その時の内容を内容を補足してまとめておきます。あくまでも私の趣味なので推奨しているわけではありませんが、JSXは関数の戻り値として扱うのに最適であり、この記事でも紹介するTransclusionやRender Propsのようなテクニックも比較的容易に書くこともでき、コードの表現の幅が広がるのは間違いありません。参考程度に目を通してもらえれば幸いです。
目次
はじめに – JSXとは
JSXは、XML(HTML)に似た構文を持つJavaScriptの拡張言語です。主にReactでView(テンプレート)を書くために使われています。仮想DOM(VirtualDOM)という言葉を聞いたことがあるかと思いますが、ReactのViewは仮想DOMで構築されており、その実体はReactElementと呼ばれるReact固有の型で成り立っています。プロパティをちょっとだけ持つだけで、メソッドもprototypeも持たず、軽量でStatelessでImmutableなため、複雑な要素の変更にも素早く対応するように作られています。
ReactElementは以下のようにReact.createElement()
関数を使って生成します。
しかし、仮想DOMを書くのに毎回React.createElement()
関数を使ってReactElement
を書くのは、冗長で、視認性も悪くメンテナブルではありません。そこでReact.createElement()
関数のシンタックスシュガーとしてJSXが用意されています。
上記のReactElementのコードは、JSXを使うと以下のように書くことができます。
Reactでは、Viewを関数の戻り値として定義するので、JSXのような書き方ができると何かと扱いやすくなります。記事の後半で紹介しますが、コンポーネント間でコードをシェアするための「RenderProps」と呼ばれるテクニックなども、JSXを使うことでシンプルに書くことができます。
Vueでも、Reactと同様、仮想DOMを使ってViewを書くことができるようになっています(Vueの内部的な仕組みとして、HTMLで書いたテンプレートは最終的に仮想DOMに変換される実装となっているためです)。そして、その仮想DOMを書くのにJSXを使うことができるようになっています。
HTMLやCSSも含めて全ての処理をJavaScriptのコード内に書くことができるようになるので、テンプレート側に条件分岐などを書く必要がなく修正漏れなども起こりにくくなりオススメです。今回の記事では、VueでJSXを書く方法を紹介します。興味がありましたら、ぜひ参考にしてみてください。
Vueにおける仮想DOM、JSX
Vueでは、createElement()
関数を使って仮想DOMを構築します。
「はじめに」のところでも書きましたが、仮想DOMを書くのに毎回createElement()
関数を使うのはしんどいです。JSXを使えば、以下のように構造が一目で理解できるように書くことができます。
Vueにはrender()
関数というものが用意されていて、このrender()
関数の戻り値として仮想DOMを返すことで、コンパイル時に仮想DOMが通常の生DOMに変換される仕組みとなっています。そしてこのrender()
関数の戻り値の部分をJSXで書くことができます。なお、JSXは素のJavaScriptのコードではないのでJavaScriptのコードにトランスパイルする必要があります。このトランスパイル時に、createElement()
関数を使ってJSXが仮想DOMに変換されます。
TypeScriptにはJSX(TSX)のJavaScriptへの変換機能が備わっていますが、Vue CLIで構築した環境ではTypeScriptだけではうまく変換ができませんでした。結局Babel(babel-plugin-transform-vue-jsxというプラグイン)を使ってJavaScriptへの変換を行う必要があります(TypeScriptオンリーで変換を行う方法があれば、教えてください)。
VueでJSXを書くための準備
Bableの準備
JSXのJavaScriptへの変換をBabelで行う必要があるので、まずVue CLIで環境を構築する際に、「Use Babel alongside TypeScript for auto-detected polyfills?」をYesとしてください。そうすることで、プロジェクト生成時にbabel-coreと@vue/cli-plugin-babelがインストールされ、babel.config.js
ファイルが生成されます。
@vue/cli-plugin-babel
には、JSXをJavaScriptに変換するためのプラグインbabel-plugin-transform-vue-jsxが含まれていて、babel.config.js
ファイルもすでに設定内容が記述されているので、JSXの変換に関するBabelの設定はこれ以上何もする必要はありません。
tsconfig.jsonの設定
tsconfig.json
ファイルは、TypeScriptのコードをトランスパイルする際に使われる設定を記述するためのファイルです。
JSXをTypeScriptで書いたファイルは.tsx
という拡張子をつけたファイル名となります。この.tsx
ファイルをトランスパイルする際の設定をtsconfig.json
ファイルに追記します。jsx
とjsxFactory
の設定で、React以外でもJSXを使用することができるようになります。
"jsx": "preserve"
とすると、TypeScriptのトランスパイル時に.tsx
ファイルが.jsx
ファイルに変換されます。preserve(保持する)としているので、記述しているJSXは変換されずにJSXのままです。
"jsxFactory": "h"
とすると、JSXの変換時にJSXのファクトリ関数としてh()
関数が使われるようになります(デフォルトは、React.createElement
です)。h
は、createElement
のエイリアスです。つまり、createElement()
関数を使って、仮想DOMが作られるようになります。ちなみに、"jsx": "preserve"
としているので、この記述はなくても問題ありません(後ほど説明するユニットテスト時に必要となります)。
詳細については、以下を参照してください。
JSXを書くファイルの準備
JSXを書くファイルを作成したら、vue
とvue-property-decorator
の両パッケージから以下のモジュールを読み込みます。
vue-property-decorator
は、Vueをクラス構文で書く際にデコレータを使えるようにしてくれるパッケージで、Vue CLIでプロジェクトを作成する際に、「Use class-style component syntax」をYesとするとインストールされます。
状況に応じて読み込むモジュールは異なってきますが、ひとまず上記のモジュールを読み込んでおくと良いでしょう。Prop
以外は、JSXを書く上では必須のモジュールとなります。
それからJSXを書くファイルのファイル名には、.tsx
という拡張子をつけて保存します。
TypeScriptでVueを書く方法について当ブログでも以下の記事を書いています。よかったら参考にしてみてください。
JSXで「Hello World」を書く
JSXを書く準備が整ったところで、早速JSXで「Hello World」を表示するコンポーネントを書いてみましょう。
SFCで「Hello World」を書く
比較として、まず通常のSFC(単一ファイルコンポーネント)で書いたHello Worldのコードを紹介しておきます。こちらもTypeScriptで書いています。
JSXで「Hello World」を書く
上記のSFCで書いたHello Worldのコードを、JSXで書くと以下のようになります。
実装のポイントは以下の通りです。
render()
関数の戻り値としてJSXで書いたコード(テンプレート)を返すようにしています。render()
の引数には、仮想DOMを生成するh()
関数(createElement()
関数)を指定しています。- スタイルは
render()
のローカル変数として定義して、JSXのstyle
属性に適用させています。
HTMLもCSSも含めてすべてTypeScript内に書くことができ、コンポーネントのすべての処理がTypeScriptのコードだけで完結するので、コードの見通しもよくなります。コード自体は、ReactやStencilを彷彿とさせるスタイルとなります。特に「デコレータ + JSX」という部分を見ると、ほとんどStencilと同じようなコードになります。つまり、VueでJSXを書くことに慣れておけば、このようなライブラリも抵抗なく扱えるようになっているはずでしょう。
JSXを使ったテクニック1 – Transclusion
Reactには、コンポーネント自身が自分の子要素にアクセスするための機能としてchildren propというものがあります。たとえばサイドバーやダイアログのようなコンポーネントは、事前に子要素として表示する内容を知らせておきたいものです。children propを使うと、直接子要素を取得することができます。一般的にこのパターンを「Transclusion(トランスクルージョン)」と言います。JSXを使うと、このTransclusionを直感的にとてもわかりやすく書くことができます。
VueでJSXを使った時に、このReactのchildren propと同等のTransclusionを実装する方法を2通り紹介します。slots(スロット)を使う方法と、関数型コンポーネントを使う方法です。
slots(スロット)を使う方法
this.$slots.default
で子要素を取得しています。子要素は配列として取得し、ここでは最初の子要素を表示させるようにしています。
関数型コンポーネントを使う方法
関数型コンポーネントにすると、render()
メソッドの第二引数にcontext
オブジェクトが渡され、そのcontext.children
で子要素を取得することができます。子要素は配列として取得し、ここでは最初の子要素を表示させるようにしています。
Transclusion実践
使い方としては、以下のように上記のChild
コンポーネントを子要素を持たさせた状態で、親コンポーネントとなるHelloWorld
コンポーネントに取り込みます。Child
コンポーネントにはそれぞれ子要素として「Hello」と「World(変数として)」を渡しています。
結果は以下の通りです。子要素からそれぞれ「Hello」と「World」を取得して、それを実装の通りそのまま表示させています。
同じコンポーネントでも、子要素に持たせる値によって別々の表示ができるようになっています。汎用的なコンポーネントを作りたい場合に、とても役立つテクニックだと思います。
JSXを使ったテクニック2 – Render Props
次に紹介するのは、昨年くらいからReactでもよく使われるようになった「Render Props」というテクニックです。関数の戻り値として扱うのにJSXは最適で、その特徴が活かされたReactでよく使われる実装パターンです。Render Propsは、抽象化したい処理を汎用的なコンポーネントに隠蔽する目的で主に使われます。
Render Propsのサンプル
まずはRender Propsで実装したサンプルを紹介します。以下の枠内でマウスカーソルを動かすとカーソルの位置情報(x, y座標)を表示します。
- Mouseコンポーネント: シェアしたい処理を記述したコンポーネントで、Render Propsを持ち、動的にレンダリングする内容を決めることができます。ここではマウスカーソルの位置情報を取得してRender Props関数の引数に渡すようにしています。
- Catコンポーネント: 実際にレンダリングされる内容を記述したコンポーネント。マウスカーソルの位置を受け取り、レンダリング内容の生成の際に使用します。ここでは、受け取ったマウスカーソル位置に合わせて、マウスカーソル位置を表示するようにしています。
- MouseTrackerコンポーネント:
Mouse
コンポーネントとCat
コンポーネントを格納する親コンポーネント。
それぞれのコンポーネントの実際のコードを以下より紹介していきます。
Mouseコンポーネント(共有したい処理)
マウスカーソルの位置情報(x, y座標)を保持し、受け取ったRender Props関数をJSX内で呼び出し、レンダリングを行います。Render Props関数はコンポーネントを返すので、コンポーネントのレンダリング内容が展開されます。今回のサンプルではCat
コンポーネントを返すようにしているので、Cat
コンポーネントのレンダリング内容が展開されます。
Catコンポーネント(共有処理を使う)
こちらのコンポーネントは、マウスカーソルの位置情報をprops
を通して受け取り、その値を使用してレンダリング内容を構成します。Render Props関数の戻り値となり、Mouse
コンポーネントの処理による値を受け取りつつ、Mouse
コンポーネントのJSX内でレンダリングされます。ちなみにCat(猫)というコンポーネント名は、Mouse(ネズミ)を追いかけるところからつけています。
MouseTrackerコンポーネント(親子コンポーネント)
上記のMouse
コンポーネントとCat
コンポーネントを格納しています。Mouse
コンポーネントにrendering
という名前で関数を渡しています。この関数は、「カーソルの位置情報を引数で受け取り」、「mouse属性からそのカーソルの位置情報を受け取るコンポーネントを返す」ようになっています。これがMouse
コンポーネントでレンダリングのために使用されるrendaring
関数の実体(Render Props関数)となります。
ここでは、もうひとつのテクニックを使用しており、MouseTracker
コンポーネントをwithSample
関数でラップしています。withSample
関数の引数を通して受け取ったコンポーネントをRender Props関数の戻り値にセットしています。このテクニックは「Higher-Order Component(HOC / 高階コンポーネント)」と呼ばれ、Render Propsと同様コンポーネントを抽象化するためのテクニックです。コンポーネントを引数で受け取り、新しいコンポーネントを返す関数だと覚えると良いでしょう。JSXを使用すると、このようなテクニックも比較的に容易に書くことができます。
注意点として、上記のコードにdeclare module 'vue/types/options'
という部分がありますが、options
型にrendering
を追加しています。これを指定しておかないと以下のようなエラーが出ました。
このエラーについてはvue-class-componentのissueにも挙げられていました。こちらも参考にしてください。
ユニットテストについて
最後にユニットテスト(単体テスト)について軽く触れておきます。VueをJSXで書いた場合のユニットテストですが、Jestで試したところ通常通り書くことができました。
なお、ユニットテストを書くときは、tsconfig.json
のcompilerOptions
のjsx
とjsxFactory
の部分を以下のように設定する必要があります。つまり、Jestでのテスト時はTypeScriptでJSXの変換を行うことになります。
上記で説明したように、実装時は"jsx": "preserve"
とする必要があります。しかしこの状態でJestを起動すると以下のようなエラーが発生しました。できれば実装時も"jsx": "react"
としてTypeScriptでJSXの変換を行いたくて、ここはいい方法がないか現在調査しているところです。
以下は上記で紹介したJSXで書いたHello WorldのJestでのテストコードです。
若干設定に難ありですが、一応ユニットテストにも対応していることがわかりました。
まとめ
VueでJSXを書く方法を紹介してきましたが、いかがでしたでしょうか。一見HTMLのようなので簡単そうに見えますが、実体はJavaScriptなので、少々ハマることもあるかと思います。ただ、関数の戻り値として扱いやすいという部分がすごく魅力的で、今回紹介したTransclusionやRender Propsのようなテクニックも比較的容易に書くことができ、コードの表現の幅が広がるのは間違いありません。興味のある方はぜひお試しください。
なお、私はTypeScriptが好きなので、今回の記事でもTypeScriptで書いたコードを紹介しましたが、まだVueでJSXを書くことにおいてTypeScriptはまだ対応が追いついていない部分があると思いました。型付けやユニットテストに関して、けっこうハマることが多かったです。現在開発中と言われているVue 3.0はTypeScriptで実装されているようなので、この辺りの対応が進むと良いですね。とにかくJSXは気持ち悪いともよく言われますが、実際に書いてみると面白いので、未トライな方はTypeScriptを使わなくても良いのでお試しされることをオススメします。
今回紹介したサンプルのソースコードは、GitHubにもアップしています。合わせてご確認ください。
コメント