Vue + JSX + Nuxt Composition API で最高のフロント開発体験

はじめに

ANDPADでフロントエンドの開発を担当している小泉です。

前回、約3ヶ月前にVue Composition APIをチームで導入して得られたメリットという記事を書かせて頂きました。

その後、今年の5月頃からまた新たなプロダクトの立ち上げを担当する機会があり、フロントの技術選定についていろいろ検討する中で、Vue.jsでもJSXを使って書けること、かなり導入しやすくなっていることを知りました。

そこで、Nuxt Composition API + TSXという組み合わせを採用してみたところ、かなり使いやすく、Vue と React のいいとこ取りができて最高 なのではないかとさえ思いました。

この記事では、そんなVue + TSX の導入方法と、メリット・デメリット、そして使う際のTipsをいくつか紹介しています。今後のフロントエンドの技術選定や、Vue + JSXでの開発に興味がある方の参考になればと思います。

(なお、Vue TemplateとJSX、ReactとVue、NextとNuxtのどちらかを否定するものではないことをあらかじめ付け加えておきます)

Vue + TSXの導入方法

環境設定

Nuxt.jsで、TypeScriptとNuxt Composition APIが導入されていることを前提とします。

まず、yarn add -D babel-preset-vca-jsx でJSXを変換するためのモジュールを追加します。

次に、nuxt.config.tsbuild.babel.presets にvca-jsxを追加します。

  build: {
    babel: {
      presets: ['@nuxt/babel-preset-app', 'vca-jsx'],
    },
  },

最後に、vuejs/composition-api: Composition API plugin for Vue 2のページで紹介されている型定義ファイル shims-tsx.d.tsを追加します。

import { VNode } from 'vue';
import { ComponentRenderProxy } from '@vue/composition-api';

declare global {
  namespace JSX {
    interface Element extends VNode {}
    interface ElementClass extends ComponentRenderProxy {}
    interface ElementAttributesProperty {
      $props: any; // specify the property name to use
    }
    interface IntrinsicElements {
      [elem: string]: any;
    }
  }
}

これだけです。

vue/jsxのページにはbabel-preset-vca-jsxを導入する手順も書かれていますが、実はこの設定自体もNuxt Composition APIに含まれています。(私もこの記事を書くために調べ直していて初めて知りました)

feat: Enable jsx support · Issue #303 · nuxt-community/composition-api

実際にTSXで書いてみる

TSXでVueを書く場合、.tsxファイルと .vueファイルのどちらも使うことができます。

例えば .tsx ファイルで、ページタイトルを表示するコンポーネントを書くと以下のようになります。

import { defineComponent } from '@nuxtjs/composition-api'
export default defineComponent({
  props: {
    pageTitle: {
      type: String,
      required: true,
    },
  },
  setup(props) {
    return () => (
      <div>
        <div class="title">{props.pageTitle}</div>
      </div>
    )
  },
})

setup関数以外の設定と、setupのreturn以外の中身は全く同じで、違いはreturnでJSX関数を返すだけです。

classをclassNameではなくclassと書くことに驚きますが、それ以外はまさにReactです。

propsや、他にrefやcomputedの .valueも自動展開されませんが、慣れればむしろこちらの方が自然に思えてきます。


見た目はReactであってもVueの別記法なので、Vue TemplateからこのJSX Componentを呼び出すことも、その逆も当然可能です。

なので、Element UIのようなコンポーネントライブラリとも問題なく併用できます。

            <el-select
              value={userIds.value}
              onInput={setUserIds}
              clearable
              multiple
              filterable
            >
              {users.map((user) => (
                <el-option label={user.name} value={user.id}></el-option>
              ))}
            </el-select>

Element UIでユーザー選択パーツを作る場合はこのようになります。(setUserIds は Reactの useState っぽい定義を自分で設定しています)

@inputonInputになっていたり、v-forの代わりにmapを使ったり、細かい記法の違いこそありますが、Vue Templateと大きな差はないことがわかると思います。

Vue + TSXの組み合わせを採用する理由

TypeScriptとの相性の良いTSXで書きたい

少し個人的な話になりますが、自分は今まで3年以上Vue.jsをメインに書いていて、ReactとJSXには何となく苦手意識がありました。

しかし、ゴールデンウィークあたりにプライベートでNext.jsを使った開発にチャレンジしてみたところ、思ったよりも違和感なく進めることができました。

特にJSX(TSX)の、あらゆるHTMLに対して型補完が利いて、書いている段階で確実にエラーになる開発体験はとても魅力的でしたし、JSX好きな方がよく言う通り、map関数や三項演算子などは直感的に使うことができます。

書きやすさの観点では「思ったほどVue Templateと差がなかった」という方が正しいかもしれません。


Vue Templateの型チェックも、VeturやVolarと言ったテンプレート解析エンジンの進化によって、ある程度は改善されてきていますが、複雑な型(union型など)で予期せぬ挙動をすることがありますし、エラーではなく警告レベルなので匙加減でスルーできてしまいます。(かといって全て禁止にするのも難しい)

また、TypeScript 3.7で導入されたOptional Chaining(?.)や Null合体演算子(??)が使えないなど、Templateが分離されていること特有のデメリットもあります。

タグの階層が深くなりすぎず簡潔に書ける、v-on.preventv-modelのような便利なヘルパーがある、などVue Templateの良さも当然ありますが、どうしてもVue Templateでなければならないという強いこだわりは自分にはなかったので、

より強固な型の恩恵を受けられるTSXが使えるならそれに越したことはないと考えました。

scriptタグは同等、styleタグはVueが優れている

一方、script部分については以前の記事でも書いた通り、Composition APIの導入でTypeScriptとの相性は全く問題なくなっています。

というよりも、React HooksとVue Composition APIでは、できることも書き方もそこまで大きな違いはない、という印象を受け、

使い慣れているComposition APIと、既存のVueプロジェクトのコードを流用できるメリットを捨てて、Reactに乗り換える強い動機は見いだせませんでした。


また、VueのSFCを使えばCSSで悩まなくて済むという利点があります。

ReactのCSS in JSは様々な選択肢があり、どれも一長一短でした。TailwindなどのCSSフレームワークをメインにすればそこまで悩まなくて済みますが、各プロダクトごとにデザイナーがいて、UXの要件も複雑なANDPADの開発においては、CSSを自分で書く場面も多く出てきます。

なるべく標準的なCSS(SCSS)で書きたい自分にとっては、VueのSFCの方が開発体験として良いと判断しました。

つまり、「HTMLはReact、CSSはVueで書くのが一番良い」という要求に、Vue + JSXがぴったり当てはまったのです。

Next.jsよりもNuxt.jsの方がANDPADの開発に適していた

VueのNuxt.jsと同様、なるべく設定周りで苦労したくない、という理由から、Reactを採用する場合はNext.jsを前提に考えていましたが、実際の運用を考えるといくつか懸念点が出てきました。

その中でも特に、標準設定ではSPAでの動的ルーティングができない点がネックで、Nuxt.jsは target: "static"ssr: falseを設定することで、静的ホスティングであっても /{id}/ のようなページに直接アクセスできますが、Nextではこの設定がありません。

現在ANDPADのフロントエンドは、Amplify Consoleを利用した静的ウェブホスティングによって運用コストを下げる流れにあるため、(Lambda@Edgeも含めて)サーバーサイドでの処理はできる限り避ける必要がありました。

また、Next/Imageによる画像最適化のような、Vercelへのデプロイを前提としている機能は使えず、評価の高いIncremental Static Regenerationについても、ログインを前提としたサービスなのでそもそも恩恵を受けられません。


一方、Nuxt.jsが提供しているモジュールシステムの面倒見の良さは、Next.jsを知ったことで改めてその有難みを感じました。

環境変数やSCSS変数の共有、GraphQLを扱うApollo Client、Google AnalyticsやSentryなどの設定まで、大抵の機能がConfigまたはモジュールとして提供されていて、nuxt.config.ts に設定を書くだけで扱えてしまいます。

Nuxtにおけるモジュールの存在は、Reactに対するVueの最大の優位点だと感じました。(なのでNuxt v3の登場を心待ちにしています!)


当然、動的ルーティングにしろGoogle Analyticsにしろ、自分でコードを書けばReact/Nextでも必ず実現できますし、Nextを使い込めばより高いパフォーマンスを実現できるかもしれませんが、Reactに慣れていない自分の場合は特に、どうしても本来の価値提供ではない部分に時間を使うことになってしまいます。

また、提供されているモジュールであれば、検索すれば誰でも使い方がわかるので、メンテナンスや引き継ぎ・レビューなどのコストを下げることにも繋がります。

業務として、特にある程度のスピード感が求められる案件であったことから、今回はNuxt.jsを使うメリットが大きいと判断しました。

Vue + TSXのデメリット

ReactほどHTMLの型定義が厳密ではない

ReactのTSXには、あらゆるHTML Elementを網羅するかなり厳密な型定義ファイルが組み込まれており、HTMLタグであっても存在しない属性や型の合わない値を渡そうとするとエラーにしてくれますが、Vue 2のTSXはそこまでではありません。

冒頭に掲載したshims-tsx.d.tsをよく見るとわかりますが、属性はstring、中身はanyになっており、例えば<div href={3}>のような定義を行ってもエラーにはなりません。

これを改善するvue-tsx-supportというライブラリもあるのですが、やや定義に不足があったり、Element UIのコンポーネントのprops定義とコンフリクトを起こしたりしたため、今回のプロジェクトでは外しています。

なお、Vue 3では型定義ファイルを内包するため、上記のライブラリは不要となっています。

まだ実際には試せていませんが、vue-nextのjsx.d.tsを見るとかなり詳細な定義がされており、Reactと同様の強力な型チェックサポートが期待できます。

React.Fragmentが使えない

実際にレンダリングしない要素のまとまりを表現したいという場合に、Vue Templateであれば<template></template>、React JSXであれば<></>で囲むことで実現できますが、Vue JSXは残念ながらどちらも対応していません。

そのため、JSXのmap関数で複数タグのまとまりをループさせることはできず、<div></div> のような余計なタグで囲む必要が出てきます。

こちらも、Vue 3 でVueにもFragmentが導入されたため、Vue 3では解消されるはずです。

Vue + TSXを使う際のTips

最後に、Vue + TSXで開発を行う際に意識している・個人的にこうした方が良いと思うことを何点か書いておきます。

.tsxではなく.vue ファイルで書く

.tsx ファイルと .vue ファイルのどちらも書けるので、Reactチックに書くのであれば .tsx ファイルを使いたくなる方もいると思いますが、個人的には .vue ファイルの方が良いと思います。

理由としては、そもそも上で書いたようにReactではなくVueを選ぶ大きな利点がstyleタグの分離にあるからです。

せっかくVueで書くからには <style lang="scss scoped"></style> が使えた方が便利ですし、<script lang="tsx"></script>で挟むだけで記述方法は全く変わらないので、拘りがなければ .vue ファイルで良いと思います。もしどうしてもVue Templateで書き直す必要が出てきた際にもファイルをリネームせずに済みます。

emitは使わない

JSXに限らずVue全般に言えることですが、Vueのemitはpropsと違って型検出や未定義の判定が実行時まで一切行われません。(Vue 3ではイベント名だけはあらかじめ指定できるようになります

また、親子コンポーネントが相互依存になってしまい、どこで処理が行われているかが分散してしまいコードを追いづらいものにしてしまいます。

そのため、emitはなるべく使わずに、propsで親の値を変更するFunctionを渡すという、Reactと同じスタイルに統一した方が、悩みが少なくなりました。

また、やり取りするデータが多くなる場合は、そもそもprop/emitを使わずに、provide/injectを使って親子間のデータ管理をコンポーネントではなく外部ファイルに移すことも検討できます。

もちろん、v-onv-modelを使うことで簡潔に書けるのもVueの強力な機能ですし、コンポーネントライブラリが使っている場合はあえて避ける必要はありませんが、TypeScriptの恩恵を最大限受けるためにemitをなるべく使わないという選択肢もあり得るのではないでしょうか。

reactiveも使わない

これもJSXに限りませんが、Vue Composition APIの問題として「refとreactiveの使い分けがわかりにくい」ことがよく挙げられます。

プリミティブな値ならref、オブジェクトはreactive、という使い分けが一般的に言われていますが、reactiveを使うと、どのタイミングでリアクティブな監視が切れるのかがわかりづらく、toRefs のようなよくわからないメソッドを挟む必要まで出てきてしまいます。

なので自分は最近では、オブジェクトも含めてrefを使って定義し、オブジェクトや配列の中身を変更したい場合はスプレッド構文などを使って中身を丸ごと入れ替えるようにしています。

これもReact的な発想に近く、useState でオブジェクトや配列の中身だけを単体で操作できなくても困らないのと同様に、全てrefで書くことによるデメリットはほとんどありません。

何なら自前の useState を作ってしまうという手もあります。

import { ref, UnwrapRef, computed, ComputedRef } from '@nuxtjs/composition-api'

const useState = <T>(
  initialState: T
): [ComputedRef<UnwrapRef<T>>, (value: T) => void] => {
  const state = ref<T>(initialState)
  const setState = (value: T) => {
    state.value = value as UnwrapRef<T>
  }
  return [computed(() => state.value), setState]
}

export default useState

providerやストア内でこのフックを使って個別にexportすることで、各コンポーネントで読み書きのどちらを行っているかが明確になりますし、

propsとしてsetState を渡すことで、emitを使わずに値の変更を提供することも簡単に可能になります。

まとめ

JSXといえばReact、というイメージを持っている方は多いと思われますが、Vue 3 ではJSXが正式にサポートされており、Vue 2 でも少しの設定で簡単にJSXを扱うことができます。

また、(以前の私と同じように)JSXに対して抵抗を持っているVueユーザーの方もいると思いますが、Vue TemplateとJSXはそこまで乖離がなく、ある程度TypeScriptに慣れていればあまり違和感なく移行できるはずです。

ReactとVueのどちらが優れているかについては明確な答えはなく、適材適所としか言えないでしょうが、

Vue.jsの最適化なしでそれなりのパフォーマンスを出せる点や、Nuxt.jsと豊富なモジュールによる環境構築の簡単さ・設定の柔軟さ、といった利点が刺さるシチュエーションは、特に業務での開発では少なくないと思っています。

Vue TemplateとJSXの比較についても同様で、Vue Templateならではの書きやすさや、HTMLとの互換性の高さといったメリットは当然あり、Vue Templateが不要になることは当面ないでしょうが、型の堅牢性を重視する大規模開発ではJSXの方が優れている場面もあります。

そこに「Vue + Composition API + TSX」という選択肢が現実的になったことで、ReactユーザーもVueを選びやすくなり、またVueユーザーもJSXを選びやすくなりました。

当然、React TSXに比べるとまだまだ未成熟な部分もあり、完全に同等とは言えませんが、その問題も多くはVue 3で解消されています。Nuxt 3がリリースされ、Vue 3への移行が進めば、自然とVue + JSXという組み合わせでの開発も増えていくのではないかと感じました。

現時点でも、既にComposition APIを採用しているプロジェクトであれば、JSX導入のハードルは本当に低いので、この記事を読んで興味を持った方はぜひ試してみて頂ければと思います!

おわりに

そして最後になりますが、ANDPADの開発チームでは一緒に働く仲間を募集中です!

engineer.andpad.co.jp

この記事のVue+TSXのような新しい技術もどんどん試すことができる環境があり、この記事では触れていませんが、同じプロダクトでGraphQL(Vue Apollo)も新たに採用しています。(こちらについても機会があればいずれ紹介できればと思います)

また、フロントエンドの基盤だけでもVue・React・Next・Nuxtと、様々な技術が各チームの特性に応じて選択されており、それぞれの良さや課題などについても活発に情報共有が行われています。

エンジニア向けのオンラインイベントやカジュアル面談なども積極的に開催・募集中ですので、興味を持った方はぜひご応募してみてください!