[勤怠FEギルド] 意外と知られていないVue.jsのRender Functionを紹介する

はじめに

こんにちは。クラウド勤怠でエンジニアをしていますkatuoです。今回は意外と知られていないVue.jsの「Render Function」を紹介します。

※ タイトルの「勤怠FEギルド」はクラウド勤怠のフロントエンドの技術的負債の解消や生産性向上のためのコード整備を行うチームのことを指しています。

Render Functionのメリット・デメリット

メリット

  • JavaScriptの力をフル活用して、プログラマティックにVue Componentを記述できる
  • ボイラープレートを減らすことができる
  • Vue.js固有の条件付きレンダリングを使わずDOM構造を記述できる

デメリット

  • DOM構造が複雑になるとコードの可読性・保守性が低下しやすい

Render Functionとは

一般的なVue.jsのコードと言うと、以下のようなtemplate構文を使ったコードが広く知られているかと思います。

※ 以下のコードはVue 3系のものです

<script setup>
import { ref } from "vue";

const count = ref(0);

function increment() {
  count.value++;
}
</script>

<template>
  <button @click="increment">Count is: {{ count }}</button>
</template>

このコードをRender Functionを使って書き直すと以下のコードになります。

import { h, ref } from "vue";

const count = ref(0);

function increment() {
  count.value++;
}

export default {
  render() {
    return h(
      "button",
      {
        onClick() {
          increment;
        },
      },
      `Count is: ${count.value}`
    );
  },
};

2つのコードの大きな違いとして、Render Functionはtemplateタグを利用せず、render関数とhメソッドを利用する点です。
後ほど詳しく解説しますが、Render Functionを使うとv-forやv-ifといったVue.js独自の条件付きレンダリングを使わず、JavaScriptを使ってプログラマティックに記述することができます。

hメソッドについて

Render Functionの中心的な存在になるのがRender Function APIとして提供されるhメソッドです。hメソッドの命名はhyperscriptから由来しています。
hyperscriptというのはHTML(hypertext markup language)を生成するJavaScriptのことを指しています。

使い方

では簡単にhメソッドの使い方を紹介します。hメソッドは第1引数にdiv, a, spanといったHTMLのタグ名, 第2引数はpropsか子要素、第3引数は子要素を渡すことができます。

// 文字列"moneyforward"をdiv要素で囲ったHTMLを出力する
h("div", "moneyforward")
// 出力
<div>moneyforward</div>

// 文字列"moneyforward"をclass, name属性が指定されたdiv要素で囲ったHTMLを出力する
h("div", { class: "hoge-class", name: "hoge-name" }, "moneyforword")
// 出力
<div class="hoge-class" name="hoge-name">moneyforword</div>

// 文字列"moneyforward"をclickイベントが指定されたdiv要素で囲ったHTMLを出力する
h('div', { onClick: () => { console.log("hoge") } }, "moneyforword")
// 出力
<div>moneyforword</div>

propsにはHTMLのattributesやEvent Listenerを設定することができます。その他にも様々な実装が可能な柔軟性の高い関数になっています。
例えば子要素が複数ある場合などどうすればいいかも公式ドキュメントにあるので御覧ください。

内部実装

hメソッドはVNodeと呼ばれるVue.jsのVirtual DOMを返すruntime-coreのメソッドです。
元々runtime-coreにはVNodeを返すcreateVNodeというメソッドが存在します。
こちらをpropsや子要素を渡せるようにラップしてより開発者が使いやすくしたのがhメソッドになります。実際のhメソッドの実装コードを見ると内部でcreateVNodeが使用されていることがわかります。

export function h(type: any, propsOrChildren?: any, children?: any): VNode {
  const l = arguments.length
  if (l === 2) {
    if (isObject(propsOrChildren) && !isArray(propsOrChildren)) {
      // single vnode without props
      if (isVNode(propsOrChildren)) {
        return createVNode(type, null, [propsOrChildren])
      }
      // props without children
      return createVNode(type, propsOrChildren)
    } else {
      // omit props
      return createVNode(type, null, propsOrChildren)
    }
  } else {
    if (l > 3) {
      children = Array.prototype.slice.call(arguments, 2)
    } else if (l === 3 && isVNode(children)) {
      children = [children]
    }
    return createVNode(type, propsOrChildren, children)
  }
}

参考: https://github.com/vuejs/core/blob/a95554d35c65e5bfd0bf9d1c5b908ae789345a6d/packages/runtime-core/src/h.ts#L174-L196

Render Functionの活用例

大前提

実際の所、template構文ではなく、Render Functionを使用する場面は少ない印象です。
その理由の1つとして冒頭にデメリットとして説明したDOM構造が複雑になるとコードの可読性が悪化しやすいがあります。

活用例のコード

活用できる場面としてコンポーネントに渡されたpropsを監視して、それを元にコンポーネントを動的に変更したい場面などがあります。

具体的なコードのイメージを持てるようにサンプルコードを書いてみました。まずは以下のようにセレクトボックスによって表示するdiv要素をリアクティブに切り替えることができるVue.jsのコードがあるとします。

<script setup>
import { ref } from "vue";
const selected = ref("");
</script>

<template>
  <div>
    <select v-model="selected">
      <option disabled value="">Please select one</option>
      <option>A</option>
      <option>B</option>
      <option>C</option>
    </select>
    <div v-if="selected==='A'" key="A">div A</div>
    <div v-if="selected==='B'" key="B">div B</div>
    <div v-if="selected==='C'" key="C">div C</div>
  </div>
</template>

上のコードはv-ifの条件付きレンダリングが並んだボイラープレートが目立つ、ハードコーディングになっています。ではこのコードをRender Functionを使って書き換えていきましょう。
まず初めにDivContainerというRender Functionを定義します。

// DivContainer.ts
import { h } from "vue";

export const DivContainer = {
  props: {
    selected: {
      type: String,
      required: true,
    },
  },
  render() {
    const $slots = this.$slots.default();
    const div = $slots.filter((slot) => slot.key === this.selected)[0];
    return h(div);
  },
};

次にDivContainerでdiv要素達をラップすることで、div要素群をDivContainerのslotsにします。

<script setup>
import { ref } from "vue";
import { DivContainer } from "./DivContainer.ts";
const selected = ref("");
</script>

<template>
  <div>
    <select v-model="selected">
      <option disabled value="">Please select one</option>
      <option>A</option>
      <option>B</option>
      <option>C</option>
    </select>
    <DivContainer :selected="selected">
      <div key="A">div A</div>
      <div key="B">div B</div>
      <div key="C">div C</div>
    </DivContainer>
  </div>
</template>

DivContainerのrender関数はpropsを元に対象のdiv要素をfilterメソッドで絞り込み、hメソッドでレンダリングする実装になっています。
このコードは初めのコードと同様にセレクトボックスの値を変更するとリアクティブに要素がレンダリングされるので、結果としてv-ifのボイラープレートを減らすことができました。

個人的にTab系のコンポーネントなどはRender Functionとの相性が良さそうな印象です。
Render Functionを活用することでpropsによって動的に要素を切り替えたり、バリデーションを実装したりすることで、JavaScriptの力の恩恵を受けながらプログラマティックにVue.jsを書くことができる場面が多々ありそうです。

最後に

今回は「Render Function」に関する解説を行いました。業務で活用できる場面があれば活用してみてください。
勤怠フロントエンドギルドチームでは毎週フロントエンドのアーキテクトについてチーム内で議論したり、フロントエンドの技術に関連する勉強会などを行なっています。
もし勤怠チームに興味があれば、お気軽にお声かけください。
最後までお読みいただきありがとうございました。

参考文献


マネーフォワードでは、エンジニアを募集しています。
ご応募お待ちしています。

【会社情報】
Wantedly
株式会社マネーフォワード
福岡開発拠点
関西開発拠点(大阪/京都)

【SNS】
マネーフォワード公式note
Twitter – 【公式】マネーフォワード
Twitter – Money Forward Developers
connpass – マネーフォワード
YouTube – Money Forward Developers

Pocket