じまろぐ

めめ

Vue.jsのrender関数(JSX)に思いを馳せた結果

この記事はVue.js #2 Advent Calendar 2018の18日目の記事です。

Vue.jsを使った開発では、特別な理由がない限り.vueファイルで記述するのが主流かと思います。.vueの場合、テンプレートの定義は<template>で行うことになるでしょう。

自分も何の疑問が湧くことなく<template>を使っていましたが、ある日ふとrender関数に思いを馳せ、いくつかの個人プロジェクトで試してみました。

本記事では、その試行錯誤から得たrender関数についてのあれこれを記しています。

<template>コンパイルされるとどうなるか

render関数の前に、まずは<template>コンパイルされるとどうなるかについて目を向けてみます。

.vue<template>コンパイルした結果どのように変換されるのか、あまり知らずにVue.jsを使っている方もいるかと思います。意識せずとも使えますし、別に知らなくても特に問題となるようなものでもありません。

<template>createElementを使った関数になる

たとえばApp.vue<template>を次のように定義します。

<template>
  <div id="app">
    <HelloWorld/>
  </div>
</template>

これがコンパイルされると、次のような関数に変換されます(見やすいように整形してます)。

var Appvue_type_template_id_54d52fb2_render = function() {
  var _vm = this;
  var _h = _vm.$createElement;
  var _c = _vm._self._c || _h;
  return _c("div",
    {
      attrs: {
        id: "app"
      }
    }, [_c("HelloWorld")], 1);
};

このように<template>createElementでVDOMを作成する関数へと変換されます。

変数名のAppvue_type_template_id_54d52fb2_renderから、これがApp.vue<template>renderにしたもの、と読み取れます。

renderといえば、render関数ですね。

render関数の基本

render関数は<template>と同じく、Vueコンポーネントにテンプレートを定義するためのものです。

簡単なコードは次のような感じになります。

export default {
  data() {
    return { msg: "hello render function"}
  },
  render(h) {
    return h("div", {}, this.msg)
  }
}

render関数は引数にcreateElementを受けるので、それをhという名前にして使うのが一般的な作法です。

render関数がコンパイルされるとどうなるか

render関数で定義したものをコンパイルするとどうなるでしょう。

次のrenderは前述した<template>の例をcreateElementを使ったものに置き換えたやつです。

export default {
  render(h) {
    return h("div", {
      attrs: {
        id: "app"
      }
    }, [h(HelloWorld)]);
  }
}

これをコンパイルすると次のようになります。

var Appvue_type_script_lang_js_ = ({
  render: function render(h) {
    return h("div", {
      attrs: {
        id: "app"
      }
    }, [h(HelloWorld)]);
  }
});

コンパイルされる前と一緒になりました。render関数でcreateElementを使って定義されたテンプレートは、変換の必要がない(そのまま実行できる)ということになります。

【ネタ】ビルド速度を突き詰めるとcreateElement!?【真似しないで】

これは計測もしてない完全な推測ですが、いろいろなものを犠牲にしてもいいなら、ビルドの速度を速くするだけのため<template>を捨ててcreateElementにする、みたいなのも考えられるかもしれません。

考えられないし絶対やらないですけど。

宣言的vs命令的

.vue<template>は宣言的にテンプレートを記述できるのが特徴であり、一つの利点です。render関数はこれと逆を行くもので、関数によってDOMを組み立てるように命令を記述します。

では命令的な記述が必ずしも宣言的なテンプレートに劣っているか?と言われると全くそうではないと思います。それぞれにメリデメがありますし、何を選択するかは使う人に委ねられているでしょう。

ただしcreateElementをそのまま使ってテンプレートを定義するのは実用に耐えるものではない(めんどくさいし読みづらい)と思います。

createElementは現実的じゃないけどJSXは結構いける

render関数ではJSXが使えます。JSXでの記述は、createElement以上<template>未満レベルで宣言的であると思います。

ちなみに、Vue CLI V3で作成したプロジェクトはBabelのプリセットに@vue/appが指定されていて、この中にJSXを使うのに必要なものが入っているので、パッケージの追加インストールなしにJSXが使えます。

export default {
  render(h) {
    return <div id="app">
      <HelloWorld/>
    </div>
  }
}

JSXを使ったrendercreateElementに変換される

render関数でcreateElementを使った場合は、コンパイルされても変化はありませんでしたが、JSXを使っている場合はcreateElementを使った関数に変換されます。

次のコードは上記JSXの変換結果です。

var Appvue_type_script_lang_js_ = ({
  render: function render(h) {
    return h("div", {
      attrs: {
        id: "app"
      }
    }, [h(HelloWorld)]);
  }
});

前述したcreateElementを使ったrender関数と同じものに変換されました。

render with JSXの可能性を探る

「VueでJSX使うくらいならReact使えばよくない?」みたいなことを何度も見たり聞いたりしますが、そんな単純な話ではないと思います。

Vueの書き味やエコシステムといった部分に乗っかりつつ、要所要所でテンプレートをJSX(render関数)で書くのは何もおかしいところはないと思います。

JSX(render関数)の可能性を探るために、<template>を使わずにJSXだけを使ってアプリケーションを作成してみました。そこから得た知見?と感想をまとめておきます。

JSXとcreateElementは共存できる

属性やイベントを細かく指定したい場合、JSXで書くよりcreateElementのほうが記述しやすい場合があります。JSXとcreateElementは共存できるので、次のような書き方もできます。

render(h) {
    return <div id="app">
      <HelloWorld/>
      {h("div", {}, "hoge")}
    </div>
  }

JSXで書いているとピンポイントでcreateElementを使いたい場面もでてくるかと思いますので、覚えておいて損はないです。

renderでしかできないこと

renderではできて、<template>ではできないことがいくつかありました。

componentsに登録せずコンポーネントを使える

importしたコンポーネント<template>で使う場合、componentsまたはVue.componentで登録しないと使えません。

render関数はJavaScriptのスコープにいるので、importしたコンポーネントは登録せずに使うことができます。前述しているrender関数の例ではHelloWorldコンポーネントを使ってますが、componentsに登録せずに使っています。

import HelloWorld from './components/HelloWorld.vue'
export default {
  render(h) {
    return <div id="app">
      <HelloWorld/>
    </div>
  }
}

使うコンポーネントが増える度に、わざわざcomponentsに登録する手間がrenderではありません。

別ファイルの定数をそのまま使える

前述したコンポーネントと同じ話ですが、別の.jsファイルに定義した定数をコンポーネント内で使う場合、computeddataに割り当ててからでないと<template>からは参照できません。

render関数ではimportした値はそのまま使えます。

import constants from './constants'
export default {
  render(h) {
    return <div id="app">
      {constants.message}
    </div>
  }
}

イベントなどをすべて定数にする

<template>@customEventな記法をするときに、このcustomEventの値を変数を参照する形で定数にしたい、と思っていろいろ試したができませんでした。

render関数では次のように記述できます。

import Channel from "../components/Channel.vue"
import types from "../store/types"
import events from "../variables/events"
export default {
  name: "Channels",
  render(h) {
    return h(Channel, {
      class: "Channel",
      on: {
        [types.REACTION_TO_MESSAGE]: this.reactionToMessage,
        [events.CLICK_REACTION]: this.reactionToMessage,
      },
    })
  },
  // ...省略
}

$emitdispatchなどに使うイベント名を定数にして、テンプレートで同じ値を参照して使うようにできます。

<template>ではcomputedでオブジェクトを返してv-onに指定することでできなくもないが、これ以外の方法があれば教えて欲しい。)

render propで<div>を挟まない

HOCに代わるパターンとしてrender propが市民権を得つつあります(もう得た?)。

Vueでrender propをする場合、スコープ付きスロット(scoped slot)を使うことになりますが、<template>はルート要素として<slot>を許容しないので、<div>で挟む必要があります。

render関数であれば、次のように記述することで綺麗にrender propできます。

render(h) {
  return this.$scopedSlots.default({
    message: this.message,
  })
}

すべてはJavaScript

<template>を使わずに書いていると、<style>も削れないかな?と思ってしまうのが人間。

そんな願いを叶えてくれるのがvue-styled-components

github.com

JSX+vue-styled-componentsを使うと、もはや.vueにする必要もなく.jsですべてを記述できます。

import styled from 'vue-styled-components'
import HelloWorld from './components/HelloWorld.vue'

const Wrapper = styled.div`
  padding: 1em;
  font-size: 1.5em;
  text-align: center;
`

export default {
  render(h) {
    return <Wrapper>
      <HelloWorld/>
    </Wrapper>
  }
}

ただし、ここまで来るとさすがに「これVueでやる必要あるか〜???」という声が自分の中から聞こえてきます。

(この組み合わせ、別に推奨してるわけじゃないです。)

Vue 3.0が来たらワンチャン?

Vue 3.0ではTypeScript対応が強化されるとのことなので、.tsxで書くのがわりと普通な状況も来そうだなと予想しています。そうなるとJSX+vue-styled-componentsも候補にあがってもおかしくないのかなと思います。

おわりに

いろいろと書きましたが、Vueのrender関数(JSX)に対する印象が少しでも変わったなら幸いでございます。