型安全に実装する、Vue 3のJSX 第1回 JSX構文の役割と基本構文

ViteでVueのプロジェクトを生成した場合、簡単にJSXを利用できるようになっています。JSXの基本的なことを確認し、Vueの開発でJSXを選択する必要性はあるか考えていきます。

発行

著者 森 大典 フロントエンド・エンジニア
型安全に実装する、Vue 3のJSX シリーズの記事一覧

はじめに

JSXは、Meta社(旧Facebook社)によって策定された、XMLやHTMLに似た構文を記述可能にするJavaScriptの拡張仕様です。Reactに採用されていることもあり、すでに利用経験がある読者もいることでしょう。

この仕様の提案には、ECMAScriptの仕様への組み込みの意図はなく、さまざまなプリプロセッサ(トランスパイラ)によって、実行可能なJavaScriptのコードに変換され利用されることを想定しています。そのため、Vueでも、Viteでプロジェクトを生成した場合は、簡単にJSXを利用できるようになっています。

しかし、JSXの仕様は「属性をもつツリー構造を簡潔で使い慣れた構文で定義する」ことのみを目的としているため、変換されるJavaScriptのコードはもとより、指定可能な属性名もReactとVueでは異なる部分もあります。一方で、Vueではテンプレート構文を使ってHTMLを記述することが一般的であり、Vueの開発者に向けたJSXのノウハウはあまり出回っていません。そのような状況で、あえてVueの開発でJSXを選択する必要はあるのでしょうか? まずは、VueでJSXを使う動機から考えてみましょう。

なお、本シリーズは、「ビルド環境で使える、Vue 3の機能」の続きとなるシリーズです。わかりにくいところがあれば、前シリーズを読むことをおすすめします。

VueでJSXを使う動機

冒頭で述べたように、JSXはJavaScriptの拡張仕様です。そのため、v-forv-ifといったディレクティブの処理も、純粋なJavaScriptの制御ロジックのみで実現できるようになります。しかし、これらのVueの基本とも言えるディレクティブは、使い方を覚えるのもさほど難しくなく、HTML出力の制御処理としても事足りることがほとんどです。これだけをもって、JSXを使う動機とするには弱いでしょう。

筆者は、JSXを採用する一番の動機は、UI定義でも型安全と型補完が効くTSX(TypeScriptと併用するJSX)の安心感と、Reactに近い実装をVueにも持ち込みたい、という開発体験上のニーズにあると考えます。

VueのSFCとReactのTSX

Vueでは、Vue 2の時代からTypeScriptを利用できましたが、テンプレートを経由することで型が無効になるため、実行時の動作不良によって初めて修正漏れに気づくという開発フローに陥りがちでした。

対してReactではTSXを使うことが一般化しており、上記のような不整合は、コンパイル時はもちろんエディタ上でも検出されるため、いち早く修正漏れを防ぐことが可能でした。それでも、Vueもバージョン3.2以降は、テンプレート上でも型が効くようになり、上記のReactと同じレベルでのエラー検出が可能になったことは、前シリーズで解説したとおりです。

また、この機能はSFCによる実装を前提としていました。そのため、CSSのカプセル化機能の選定に悩む必要はなく、Scoped CSSやCSS Modulesをすぐに使え、テンプレートにはHTML本来の構文による記述が可能でした。これらの点を踏まえると、すでにVueは、TSXベースのReactを超える開発体験を提供していると捉えることができるかもしれません。

SFCでの型の利用条件とTSXの必要性

TSXがなくとも、Vueならでは利点を享受しつつ型安全な開発が可能になったわけですが、筆者はそれでも、VueでTSXを使う必要性はないとは言い切れません。

上述した機能の利用には、VS Codeの拡張機能であるVolarの導入が必須です。また、Vue 2時代の構文を継承したProps定義は、Reactユーザーにとっては煩雑に映るかもしれません。definePropsによってReactに近い定義が可能にはなったとはいえ、コンパイルマクロ特有の実装上の注意点もありました。

これらの点は、Volar非対応のエディタを使っている、あるいは、TSXベースのReactの実装を好む開発者にとっては、Vueの開発でもTSXを使いたいと思わせる理由になり得るでしょう。

そこでは本稿では、上記のような開発者、あるいは、JSXを用いた開発に興味がある人向けに、VueプロジェクトにおけるJSXの利用方法を解説していきます。なお、記事タイトルではJSXと謳っていますが、TSXをベースに解説していきます。

JSXの利用準備

以降、試しながら進められるよう、ViteベースのプロジェクトでJSXを使う準備をしておきましょう。

まずは、npm init vite@latestコマンドでTypeScriptベースのVueプロジェクトを生成します(プロジェクト生成方法の詳細は前シリーズを参照ください)。プロジェクト生成条件時のSelect a variantで、TypeScriptを指定しましょう。

次に、ViteのプロジェクトでJSXを使うには、@vitejs/plugin-vue-jsxというプラグインが必要になるためインストールします。

JSXプラグインのインストール

npm i @vitejs/plugin-vue-jsx -D

そして、プロジェクトルートに配置されたvite.config.ts内の設定で、このプラグインを読み込むようにします。

JSXプラグインの読み込み設定

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx'; // 追加

export default defineConfig({
  plugins: [
    vue(),
    vueJsx(), // 追加
  ],
});

これでコンポーネントファイルの拡張子を.tsxにすることで、JSXを利用できるようになります。なお、上記設定で読み込んでいる@vitejs/plugin-vueは、SFCの.vueファイルの利用に必要となるプラグインです。.tsxとの併用も可能ですが、意図的に.vueを利用できないようにする場合は、除去しておくとよいでしょう。

VueにおけるJSXの役割

記事の冒頭にて、JSXで記述されたコードは、プリプロセッサによって実行可能なJavaScriptのコードに変換されると述べました。後述するJSXならではの実装方法を理解するためにも、まずは、JSXがどのようなコードに変換されるのかを押さえておきましょう。

h()関数とJSX

まず、JSXの前に、次のコードを見てください。

App.ts

import { h } from 'vue';

export default () => {
  const vnode = h('a', { href: 'https://www.google.com' }, ['Google']);
  return vnode;
};

これは、プロジェクトのデフォルトで配置されるApp.vueの拡張子を.tsに変更し、その中身を書き換えたものです。

このコード内にあるh()関数は、hyperscriptの略で「HTMLを生成するJavaScript」という意味があります。その言葉のとおり、引数には、Googleのサイトへリンクするa要素の構成値が、次の順で指定されています。

h()関数の引数

h(
  'a', // 要素名
  { href: 'https://www.google.com' }, // {属性名: 属性値}
  ['Google'] // [テキスト]
)

次にJSXのコードを見てみましょう。実は上記のコードは、拡張子を.tsxにしJSXを使うと、次のように書き換えることができます。

App.tsx

export default () => {
  const vnode = <a href="https://www.google.com">Google</a>;
  return vnode;
};

h()関数によるHTMLの表現と比較すると、JSXの仕様で謳われている「簡潔で使い慣れた構文」になっていることがわかります。そして、アプリケーションを実行すると、次のようにHTMLが表示されます。

この結果からも推測できますが、JSXやテンプレート構文というのは、Vueではh()関数を使った実装のシンタックスシュガー(糖衣構文)として機能します。つまりは、ビルド処理を経由し生成されるコードは、h()関数を呼び出す実装に置き換わるということになります。

HTML定義のJSON化

では、h()ではどのような処理が行われ、どのような値を返しているのでしょう? 実行結果としてHTMLが表示されるのをみるところ、指定された要素のDOMオブジェクトの生成でもしているのでしょうか?

上記コードで、h()、JSXの返却値を格納している変数vnodeの値を、DevToolsのコンソールに出力し確認してみると、結果はいずれも次のようになります。

変数vnodeの値

{
  "type": "a",
  "props": {
    "href": "https://www.google.com"
  },
  "children": [
    {
      "children": "Google",
    }
  ],
  ...
}

省略している部分

上記のJSONでは、...の部分は省略していますが、実際には、keyrefなどの多くのプロパティが追加されます。また、これらのプロパティ定義やデータ構造は、VueやReactといったフレームワークごとに異なります。

JSXで定義した<a>要素が、その構成値をもつJSONオブジェクトに変換されていることがわかります。

仮想DOMツリーへの変換

上記の変数vnodeに格納したJSONは仮想ノードと呼ばれるもので、リアクティブシステムの基盤となる仮想DOMツリーを構成するノードの1つにあたります。このことは、次のような要素がネストしたJSXがあったとき、

要素がネストしたJSXの定義

const vnode = (
  <div>
    <a href="https://www.google.com">Google</a>
  </div>
);

次のようなノードがネストされたJSON、つまり仮想DOMツリーに変換されることを意味します。

変数vnodeの値

{
  "type": "div",
  "props": null,
  "children": [
    {
      "type": "a",
      "props": {
        "href": "https://www.google.com"
      },
      "children": [
        {
          "children": "Google",
        }
      ],
      ...
    }
  ],
  ...
}

Vueは、まず、このJSONのプロパティを末端まで走査することで、ページ全体の描画を行います。そして、リアクティブシステムによりツリー内の値変更を検知したとき、その差分のみを実際のDOMに反映し再描画するというわけです。

これらのことから、JSXのVueにおける役割は、JavaScript内での仮想DOMツリーのノード生成処理の直感的な記述にあるといえます。

JSXの基本構文

JSXの実態がわかったところで、基本構文についても見ていきましょう。

属性値とテキストの動的指定

変数や引数に保持された値を、HTML要素の属性値やテキストノードとして、JSXの定義の中で割り当てたいケースもあるでしょう。その場合は次のように、中括弧({})で囲んで記述します。

中括弧による値の指定

const href = 'https://www.google.com';
const text = 'google';
const vnode = <a href={href}>{text}</a>;

イベントハンドラの割り当ても、同様に中括弧を使いますが、イベントの属性名はキャメルケースで記述します。

イベント属性名のキャメルケース(onClick)での記述

const addOne = () => { ... };
const vnode = <button onClick={addOne} >+1</button>;

要素の動的挿入

JSXの実態は仮想ノードと呼ばれるJSONであり、childrenというプロパティでノードをネストすることでツリー構造を表現していました。それであるならば、UI構成の一部にあたる仮想ノードを定義しておき、仮想DOMツリーの任意の箇所(children)に挿入するといったことも可能でしょう。

これをJSXで行うには、前述同様に中括弧を使用し、任意の箇所に仮想ノードが格納された変数名を記述するだけで済みます。

定義済み要素の挿入

const googleLink = <a href="https://www.google.com">Google</a>;
const vnode = <div>{googleLink}</div>;

h()で生成したJSONデータを直接変更する実装を想像すると、JSXの恩恵をより感じられるでしょう。

また、このような実装を見ると、UIの一部を変数に格納しておき、複数箇所に配置するというアイデアを思い浮かぶかもしれません。

定義済み要素の複数箇所への挿入

const siteLink = <a href="..."><img src="..."></a>;
const vnode = (
  <div>
    <header>
      { siteLink }
      ...
    </header>
    <main>...</main>
    <footer>
      <ul>
        <li>{ siteLink }</li>
        ...
      </ul>
    </footer>
  </div>
);

このような、ローカル変数に格納したUIの一部を使い回すといった実装は、テンプレート構文では得られないJSXならではの利点といえるでしょう。また、上記のsiteLink変数に格納されたJSONは、仮想DOMツリーのノード走査において、同一オブジェクトとして2回登場することになります。しかし、リアクティブデータの影響を受けない静的なJSONであるため、このような使い方をしても特に問題は生じません。

ここまでのまとめ

VueにおけるJSXの役割は、仮想DOMツリーを構成するノードにあたるJSONデータであることがわかりました。そして、単なるJSONデータであるがゆえに、テンプレート構文のようにUIの構成要素をまとめて定義することも、あるいは分割定義しておき、組み合わせて利用することも可能でした。

この定義済みUIを組み合わせる利用と聞くと、コンポーネントを思い浮かべないでしょうか? 定義済みJSXとは、コンポーネントと同じものなのでしょうか。そこで次回は、定義済みJSXとコンポーネントの関係と、JSXベースのコンポーネントの特性や実装方法について解説していきます。