Nuxt3×TSXで消えるVueっぽさ
この記事は すごくなりたいがくせいぐるーぷ 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の設定を書き換えます。
{
// https://v3.nuxtjs.org/concepts/typescript
"extends": "./.nuxt/tsconfig.json",
}
nuxi init
で自動作成されたtsconfig.json
はこのようになっています。./.nuxt/
以下にNuxtが自動生成する型の情報などが入っているため、それを読み込んでいるようです。ここに、"jsx": "preserve"
を追加します。
{
// https://v3.nuxtjs.org/concepts/typescript
"extends": "./.nuxt/tsconfig.json",
+ "compilerOptions": {
+ "jsx": "preserve"
+ }
}
これでTSXを書くための事前準備は終了です。
nuxt.config.ts を編集してもOK
nuxt.config.ts
のtypescript.tsConfig
オプションを書き換えることでもTSXを有効化できます。
このオプションは./.nuxt/tsconfig.json
に反映されます。
階層も深くなるので、通常はtsconfig.json
の変更で十分だとは思いますが...。
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で書き換えてみましょう。
<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を使うことによりより厳格な型チェックが効くはずです。
続いて、実際に自分でコンポーネントを定義して使ってみましょう。
<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>
<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」を書くことはできるのでしょうか。
export default defineComponent({
props: {
name: {
type: String,
default: null
}
},
setup(props) {
return { name: props.name };
},
render() {
return <div>Hello {this.name}!</div>;
}
});
import HelloWorld2 from "~/components/HelloWorld2";
できました。先述のとおり、defineProps
は<script setup>
内でしか利用できないので、「完全なTSX」では使えませんから、いちいちprops
を書かなくてはいけません。辛い...。
また、import
の際、拡張子.tsx
は省略します。
...これもまだVueだと思うあなたにこちらのコンポーネント。
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つ紹介します。
style
属性
1. Vue3のstyle
属性は、Reactのようにオブジェクトを渡すことができるようになりました。
<div style={{ color: "red" }} />
ただし、当然Emotionなどを使う場合とは異なり、style
属性の値が長く記述されるだけです。
2. CSS Modules
CSS Modulesを使うには、さらにtsconfig.json
の書き換えが必要です。
{
// https://v3.nuxtjs.org/concepts/typescript
"extends": "./.nuxt/tsconfig.json",
"compilerOptions": {
"jsx": "preserve",
+ "types": ["vite/client"]
}
}
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で型安全なクリスマス・年末年始を送りましょう!
作業内容がまとまったリポジトリはこちらから
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