🌄

Nuxt3×TSXで消えるVueっぽさ

2021/12/20に公開約7,200字
この記事は すごくなりたいがくせいぐるーぷ Advent Calendar 2021 21日目の記事です。

はじめに

Nuxt3のベータ版登場から2ヶ月。

We are targetting to start RC releases of Nuxt3 until January.
Timeline for stable release · Discussion #2198 · nuxt/framework

とのことで、正式なリリースも近づいていそうです。
そんなNuxt3は、ドキュメントに記述がまだないものの、TSXによるテンプレートの記述をサポートしています。ということで、実際にTSXを使ってみたいと思います。

1. 環境構築

まずはプロジェクトを準備。
ここでは、nuxt3-tsxディレクトリ内に作成します。

npx nuxi init nuxt3-tsx
cd nuxt3-tsx
yarn install
yarn dev -o # -o をつけるとブラウザが自動で開く


組み込みの<NuxtWelcome>コンポーネントの内容が表示される
nuxi initはNuxt2までとは違って質問攻めがないのが嬉しいですね。
この画面が表示されていれば環境構築は完了です。早い...!

2. TSXを有効化する

つぎに、TSXを使えるようにTypeScriptの設定を書き換えます。

tsconfig.json
{
  // https://v3.nuxtjs.org/concepts/typescript
  "extends": "./.nuxt/tsconfig.json",
}

nuxi initで自動作成されたtsconfig.jsonはこのようになっています。./.nuxt/以下にNuxtが自動生成する型の情報などが入っているため、それを読み込んでいるようです。ここに、"jsx": "preserve"を追加します。

tsconfig.json
{
  // https://v3.nuxtjs.org/concepts/typescript
  "extends": "./.nuxt/tsconfig.json",
+ "compilerOptions": {
+   "jsx": "preserve"
+ }
}

これでTSXを書くための事前準備は終了です。

nuxt.config.ts を編集してもOK

nuxt.config.tstypescript.tsConfigオプションを書き換えることでもTSXを有効化できます。
このオプションは./.nuxt/tsconfig.jsonに反映されます。
階層も深くなるので、通常はtsconfig.jsonの変更で十分だとは思いますが...。

//storage.googleapis.com/zenn-user-upload/30646126f334-20211220.png)s
import { defineNuxtConfig } from 'nuxt3'

// https://v3.nuxtjs.org/docs/directory-structure/nuxt.config
export default defineNuxtConfig({
+ typescript: {
+   tsConfig: {
+     compilerOptions: {
+       jsx: "preserve"
+     }
+   }
+ }
})

3. TSXで書いてみる

まずは、app.vueをTSXで書き換えてみましょう。

app.vue
<script lang="tsx">
import NuxtWelcome from "nuxt3/dist/app/components/nuxt-welcome.vue";

export default defineComponent({
  render() {
    return (
      <div>
        <NuxtWelcome />
      </div>
    )
  }
})
</script>

このとき、<NuxtWelcome />について、JSX 要素型 'NuxtWelcome' にはコンストラクトも呼び出しシグネチャも含まれていません。ts(2604)というエラーが出ますが、これはNuxtWelcomeの型定義がTSXに対応していないというだけのことです。ここでは一旦無視します。

さて、Vueのテンプレート記法との大きな違いは、

  • コンポーネントをimportしている
  • <template>ではなく<script>内で定義している

ことです。TSXを使うとどうしてもコンポーネントの自動読み込みが効かなくなるようで、Nuxtの良さが半減してしまう感じはあります。一方で、TSXを使うことによりより厳格な型チェックが効くはずです。


続いて、実際に自分でコンポーネントを定義して使ってみましょう。

components/HelloWorld.vue
<script setup lang="tsx">
interface Props {
  name: string;
}
const { name } = defineProps<Props>();
</script>

<script lang="tsx">
export default defineComponent({
  setup() {
    return { name };
  },
  render() {
    return <div>Hello {this.name}!</div>;
  }
});
</script>
app.vue
<script lang="tsx">
import HelloWorld from "./components/HelloWorld.vue";
export default defineComponent({
  render() {
    return (
      <div>
        <HelloWorld name="World" />
      </div>
    )
  }
});
</script>


Hello World! この先ずっと同じ表示です。

Nuxt3では、definePropsを使ってPropsの型定義ができます。便利!!
では、いくつかポイントを見ていきましょう。

<script setup>がうまく働いていない

HelloWorld.vueを見ると、<script setup>を利用しているのに、defineComponent内で再びsetup()を呼び出しています。TSXを使うと、なぜか<script setup>内の変数などが解決されないようです。
ちなみに、最初からsetup()内に書くこともできますが、definePropsなど一部は<script setup>内に書かないと動きません(後述します)。

また、this.nameのように、変数がthisのプロパティとして扱われるのがTSXで書くときの特徴の一つです。

使うときはコンポーネントをimport

やはりimportしないと型が解決されません。
ですが、importさえしてしまえばpropsの型についても厳格に推論してくれます。

4. 「完全なTSX」を書きたい

(この記事における「完全なTSX」とは、VueのSFCではなく、拡張子.tsxのコードを指します。)
ここまで扱ってきたのはあくまで<script lang="tsx">、つまりVueのファイルでした。では、「完全なTSX」を書くことはできるのでしょうか。

components/HelloWorld2.tsx
export default defineComponent({
  props: {
    name: {
      type: String,
      default: null
    }
  },
  setup(props) {
    return { name: props.name };
  },
  render() {
    return <div>Hello {this.name}!</div>;
  }
});
app.vue
import HelloWorld2 from "~/components/HelloWorld2";

できました。先述のとおり、defineProps<script setup>内でしか利用できないので、「完全なTSX」では使えませんから、いちいちpropsを書かなくてはいけません。辛い...。
また、importの際、拡張子.tsxは省略します。

...これもまだVueだと思うあなたにこちらのコンポーネント。

components/HelloWorld3.tsx
interface Props {
  name: string;
}
export default (props: Props) => <div>Hello {props.name}!</div>;

Vueが跡形もなく消えました。これも動きます。

ですが、この書き方をすると、refの扱いが少しだけ面倒になります。

const text = ref("World");

を定義した場合、setupを使う場合はtextがそのまま値(string)になりますが(unref(text)相当)、使わない場合はtext.valueを参照しなければなりません。

おまけ TSXでスタイルを書くには

「完全なTSX」ではもちろん<style>タグは使えません。ではどのようにスタイルを書くか。ここでは2つ紹介します。

1. style属性

Vue3のstyle属性は、Reactのようにオブジェクトを渡すことができるようになりました。

<div style={{ color: "red" }} />

ただし、当然Emotionなどを使う場合とは異なり、style属性の値が長く記述されるだけです。

2. CSS Modules

CSS Modulesを使うには、さらにtsconfig.jsonの書き換えが必要です。

tsconfig.json
{
  // https://v3.nuxtjs.org/concepts/typescript
  "extends": "./.nuxt/tsconfig.json",
  "compilerOptions": {
    "jsx": "preserve",
+   "types": ["vite/client"]
  }
}
nuxt.config.ts
import { defineNuxtConfig } from 'nuxt3'

// https://v3.nuxtjs.org/docs/directory-structure/nuxt.config
export default defineNuxtConfig({
  typescript: {
    tsConfig: {
      compilerOptions: {
        jsx: "preserve",
+       types: ["vite/client"]
      }
    }
  }
})

こうすることで、コンポーネントでCSSの読み込みが可能になります。

import styles from "./styles.module.scss";

<div class={styles["text-red"]} />

CSS Modulesとして使うファイル名には.moduleをつけます。
なお、SCSSを利用する際はsassパッケージを別途インストールする必要があります。

おわりに

このように、Nuxt3ではTSX記法が(ほぼ完全に)サポートされています。
VueのテンプレートよりもTSXが書きたい、そんな方にはおすすめです。
特にReactとVueを行き来するような人には便利かもしれません。余談ですが、ReactのuseStateとNuxt3のuseStateは役割が異なります。名前を変えてほしかった。
Nuxt3とTSXで型安全なクリスマス・年末年始を送りましょう!

作業内容がまとまったリポジトリはこちらから

https://github.com/e-chan1007/1221-nuxt3-tsx-example
TSXを使う意味を考える

この記事を書くにあたって、TSXを書く意味をずっと考えていました。

TSXを使うことによりより厳格な型チェックが効くはずです。

という記述をしましたが、これが理由になるとは考え難いところがあります。
Nuxt3とVolarの組み合わせが強すぎて、素の<template>でも型推論が十分に効いてしまうのです。
さらにTSXを使う欠点を考えてみると、

  • <script setup>と一緒に使いにくい
  • <style>を同一ファイルに書けない(後述)
  • コンポーネント型が自動で読み込まれない
    • 組み込みコンポーネントも読み込まれない

などが考えられます。なんだかNuxtの良さを片っ端から潰しているような気がしますね。
タイトルには「Vue感が消える」なんて書いていますが、個人的には消したくはなかった...。

一方で、コンポーネントをmapで記述したり、onClickなどの引数に関数をがっつり渡せる、などの利点もTSXにはあると思いますから、それをメリットと捉えられるのであればNuxt×TSXはかなり強力な道具になるでしょう。
また、ReactはTSX、VueはSFCというように適材適所的な考え方もある気がしています。

とても個人的な話をすると、Reactのstateは配列にpushするにもsetStateし直さなければならないのがとても面倒です。Vueでpushできるのは幸せですよね。そういったところで、ReactとVueの中間のような感じもしますね。これからReactを書こうとしている人にもNuxt×TSXを勧められる気がします。

Discussion

ログインするとコメントできます