まえがき
Vue.jsでUIを構築していく際にはHTMLライクに書けるSFC(Single File Component)の<template>
タグでコードを書いていくのが基本ですが、
templateタグだけでは困る場面が出てきます。(例えばSFCのルートエレメントを動的に変えることは出来ません)
そうした場面のためにVueではrender関数を利用したUI構築ができるようになっていますが、
標準のrender関数で利用するcreateElementを使った書き方はコードの量が多くそれほど快適ではありません。
そこでrender関数を利用しつつも、快適にHTMLを書くためにVueでもJSXを使うことができるようになっています。
ただし、Vueのtemplate記法とJSXを利用した場合の構文は似て非なるものになっているのでこの記事はその移行ガイドです
とはいえ、Vueにある程度習熟してきた方にとっては一通りのルールを覚えるだけですぐに書けるようになるのでJSXを部分的に取り入れるハードルはそれほど高くないと思います。
JSの中にHTMLが入ることでエディタのコード補完が強く働くなど嬉しいことも多いです。
なおサンプルのコードは自明な部分は一部省略していますので、コピペで完動するものではありません
JSX is ¿¿¿???
JSX はJavaScript XMLからうまれた名前で、HTMLをXMLとして扱う+JSの力を使って構築ができる、という技術です
Reactで主に使われていますが、Reactと密結合しているわけではなく個別に利用することが出来ます。
また、TypeScriptと組み合わせて使う際にはTSXと呼ぶこともあります。
プロジェクトでJSXを利用できるようにするための設定はさほど難しくないので公式ドキュメントに応じてやってください
webpackの設定とbabelrcとtsconfig(TypeScript利用時) をちょっと変える必要があります。
sandbox
以降の内容について、実際にどんな感じでJSXが書けるかを試したいときはこちらのCodeSandboxを使ってください
https://codesandbox.io/s/vue-template-lk0w6
コンポーネントの作り方
SFCを普通に作りますが、templateを書かずに、vue本体の方にrender関数を用意します。
そしてrender関数の中からJSXをreturnするコードを書きます
JSXのブロックは()
で囲って書きます
renderの引数のh
は何も利用していないように見えますが、書かないとビルドに失敗したりするので書いておくほうがいいです
(hを省略してもプラグインが自動挿入してくれるはずなのですが、時折手元でエラーになることがある)
new Vueをする場合
new Vue({
render(h) {
return (
<div />
);
}
})
コンポーネントファイルを作る場合
<script>
export default {
data() {
return {
counter: 0,
focusing: false
}
},
render(h) {
return (
<div></div>
)
}
};
</script>
拡張子を.vueではなく.jsxにして<script>
タグを外すことは出来ますが、その場合にはSFC風の<style>
なども使えなくなってしまうので、別の手段をなにか用意する必要があります。
原則
VueのSFCではHTMLの中にダブルクオートで囲った領域を書くと、そこがJSの式として評価されるようになっていましたが、JSXでは{}
で囲います。テンプレート内でのバインディングも同様です。
また、変数やメソッドにアクセスする場合には、あくまでrenderというJSの関数の内部の処理を書いているので、vueで処理を書くのと同様に、thisで各種変数や関数にアクセスをするようになります
<script>
export default {
data() {
return {
counter: 0,
}
},
render(h) {
return (
<div class="content">
<div> count: {this.counter}</div>
</div>)
}
};
</script>
文法
基本的にはgithubのREADME.mdを参照します。
v-onなどケバブケースで表記していた属性をvOnのようにキャメルケースで書いていくのが基本的な違いになります
HTMLがベースにあるtemplate記法と、JSがベースにあるJSXの違いの一端が見えてちょっとおもしろい部分です。
大原則としてJSXはVueのrender関数の標準であるcreateElementをラップしているだけなので、
createElementとJSXの変換さえできれば、基本的にはなんでもできます
v-bind
template記法ではダブルクオートで囲っていましたが、{}
で囲います
また先頭の:
は不要です
<div :class="this.$style.red">style with css modules</div>
<div class={this.$style.red}>style with css modules</div>
v-on
v-onの省略記法である@
は使うことが出来ません
JSXの標準のイベントハンドリング、もしくはvOn
を使うことが出来ます
vOnに関してはbabel-sugar-v-onプラグインに従った動作をします。
以下すべてのbuttonタグは同じように動作します。
template記法と違って、直接処理を書いても正常に動作しないので、必ず関数オブジェクトの形で値を設定しなければいけません
<button onClick={() => this.counter++}> increment</button> // JSX標準のイベントハンドリング
<button vOn:Click={() => this.counter++}> increment</button> // 推奨されているJSX版v-on
<button v-on:Click={() => this.counter++}> increment</button> // 非推奨のJSX版v-on
v-if
v-ifは使うことが出来ません。またJSXにもv-ifと同等なものはありません。
条件分岐により描画の有無をコントロールしたい場合には、JSの力を利用して3項演算子などで書きます。
{this.counter > 0 && (<div>{this.counter}</div>)}
JSXの条件分岐に関する記事は世に多く出ていて、それがそのまま参考にできるのではないかと思います。
https://qiita.com/SLEAZOIDS/items/4f3c1b77d291eef548af
v-for
v-forのような繰り返しを記述する場合には、JSのmapなどを利用して繰り返し処理を書きます
{Array.from(Array(5).keys()).map(x => {
return <span>{x}</span>;
})}
v-model
v-modelは通常のtemplate記法とほぼ同じように利用することが出来ます。
これはbabel-sugar-v-modelによって動作しています
v-modelと書いても動くようですが、vModelが推奨になっています。
<input vModel={this.counter} />
slot
<slot>
タグは利用できないので直接$slots
変数にアクセスする形になります
<app-child>hogehoge</app-child>
<script>
export default {
render(h) {
return <div>{this.$slots.default}</div>;
}
};
</script>
名前付きslot
<app-child><template slot="test">hoge</template></app-child>
render(h) {
return <div>{this.$slots.red}</div>;
}
slot-scope
scoped-slotでもslot同様に$scopedSlotsに直接アクセスして処理を書きます
slotという名前がついていますが、HTML上は入れ子にせずに、それぞれのscopedslotに対してpropsをJSXに変換する関数を定義します。
<app-child
scopedSlots={{
default: ({ log }) => {
return <button onClick={log}>test</button>;
}
}}
/>
<script>
export default {
render(h) {
return <div>{this.$scopedSlots.default({ log: this.log })}</div>;
},
methods: {
log() {
console.log("logging");
}
}
};
</script>
スタイルのバインド
拡張子が.vue
である限り、今までと同じようにscoped cssもしくはCSSModulesの利用が可能です
<script>
render(h) {
return (
<div class="content">
<div class={this.$style.red}>style with css modules</div>
<div class="blue">style with scoped css</div>
</div>
);
}
</script>
<style scoped>
.blue {
color: blue;
}
</style>
<style module>
.red {
color: red;
}
</style>
ユースケース
最後に個人的にJSXが特に有効だと考えているいくつかの状況を少しだけ取り上げます
ルートエレメントのタグを動的に変更する
Vueのtemplate記法はルートにある要素のタグを動的に決めることが出来ません。
このため、OSSで提供されている一定規模以上のUIコンポーネントライブラリのソースなどを見ていると、まずtemplateタグが出てきません
この制約を回避するにはrender関数を使うしかありませんが、その場合にJSXを使うとコードが書きやすいというメリットがあります
<app-child tag="div"/>
render(h) {
const tag = this.$props.tag; // 一度render関数の中で変数として定義する
return <tag>aa</tag>; //ルート要素のタグを変数にできる
},
props: {
tag: {
type: String,
default: "div"
}
},
storybookのコードをJSXで書く
もはやデファクトスタンダードの地位を確立しつつあるstorybookですが、個別のstoriesの記述がテンプレートリテラルで書かないといけないため、記述に苦労します。
そこをJSXで書くとlintによる補正も効くようになるためコードの記述がかなり楽になります。
注意点としては以下の2点があります。
- storybook-addon-knobsの値の書き方に少し工夫が必要
- propsのデフォルト値としてknobの定義を書くようにしないとエラーになります。
- storybook-addon-vue-infoとJSXの相性がよくないようで、infoを諦める必要があるかもしれません
- 公式のissueでは、renderの中でthisにアクセスすることができないと言われています
とはいえ、素の状態に比べると遥かに記述が楽になるのでこれはおすすめできます。
Vue本体のコードは普通にtemplate記法で書くけど、storiesだけはJSXでかくというような構成にすると本番のコードにも依存関係が増えず有効です
stories
.add(
`SampleComponent`,
() => {
return {
components: { SampleCompoent },
render() {
const tag = this.$props.tag
return (
<tag>
<sample-component />
</tag>
)
},
props: {
tag: { default: select('tag', ['div', 'ul']) },
},
}
},
{
notes: README,
}
)