はじめに
はじめまして。
2019年3月に入社した正岡です。
平均週に4回、大分県中津のから揚げ弁当を食べている大の鶏好きです。からあげクンと呼んでください。
主に Rails × Typescript のプロジェクトにフロント/バックエンドエンジニアとして参加しています。今後ともよろしくおねがいします。
本記事の内容
およそ1年前、弊社ではDocBaseのユーザーの皆様により良いユーザー体験をしていただくために何ができるかを検討しました。結果、フロントエンドを改修して SPA(Single Page Application) に移行することで、従来よりも速く、快適にご利用いただけるのではないかという結論に至り、ご存知の通り先日リリースいたしました。
フレームワークどれを使うか問題
既存サービスのSPA化などのフロントエンド改修を検討する際、「フレームワークどれ使う?」問題にぶち当たりますよね。「react vue 比較」みたいにググると非常に多くの情報を得ることができますが、結局どちらにもメリット/デメリットがあるのでどっちにしようか決めかねている方も多いと思います。
弊社でも多分に漏れず、ReactかVueどちらにしようか悩みに悩みました。悩んだ末、「まだ使ったことないからチャレンジしよう!」という理由で React を選定することにしました。
なので、すでに提供されている素晴らしい選定基準については他の方の記事にお任せすることにして、この記事では、プログラマーのQOLの部分で最も重要な 「どっちがより楽しいか?」 という観点から選定の材料を提供してみたいと思います。開発メンバーのテンションを保つ上で重要な選定観点ですね。
テーマ的に大変主観的な内容になりそうな恐れはありますが、極力納得いただけるように実務上の経験から具体例を示そうと思います。
環境
FW | メジャーバージョン | プロジェクト規模感 | With Typescript? |
---|---|---|---|
React | 17 | DocBase | Yes |
Vue | 2 | 1ページ1機能程度の簡単なページが20枚程度 | Yes |
Vueについてはすでに v3 がリリースされていますが、Vueを使ったプロジェクトにジョインしていた当時、まだ v2 が最新だったため比較対象は v2 となっています。エアプで v3 の情報について発信するのはおこがましいので、 v3 についての言及は控えます。
なお、どちらのプロジェクトもバックエンドは Ruby(Rails) を採用しました。
Reactの方が楽しかった
まずは結論。
Reactの方が1.2倍楽しかったです。異論は認めます。
理由をひとことでいうと、JSに集中してコードを書くことができたからだと思います。
詳しくは後述に任せますが、Vueでストレスを感じることがReactではすっきり解消されていて、これといって実装中にReactを選定したことが原因のストレスはあまり感じなかったと思います。
「ストレスフリー」=「楽しい」は消極的な理由に聞こえるかもしれませんが、余計なことにストレスを感じずにプログラミングを楽しめるのは何よりのことだと思います。
それでは詳しく見ていきましょう。
Reactが楽しい理由
JSXマジでいいよ
Reactを書いていて楽しい理由は、JSXに依るところが非常に大きいと思います。
ほか記事で散々JSXについて読んでいたとしても、ものは試しに触ってみてほしい。
JSXとは
Reactを使うなら切っても切り離せない関係にあるのがJSXです。Reactでは基本的にJSXを使います。
ReactではJSとHTMLの垣根を超えてシームレスにJSを書けますが、その理由はJSXにあります。
ではJSXとはなにかというと、公式によると
JSX is an XML-like syntax extension to ECMAScript without any defined semantics.
つまり、JSXとは「意味を定義しない、XMLのようにJSの書き方を拡張するもの」。もっと意訳すると「JSをもっと便利に書けるようにしたもの」ということですね。
定義だけでは具体的なイメージができないので、JSXを使ったらどんなふうに便利なのか、例を見てみましょう。
簡単な例
JSXを使った最も簡単な表現は例えばこれ。
const element = <h1>Hello, world!</h1>
(React公式より)
JSの変数にHTMLの要素が代入されていますね
代入したものはこんな感じで使い回すことができます。
const hello = <p>Hello, World!</p>
ReactDOM.render(
<>
{hello}
{hello}
{hello}
</>,
document.getElementById("root")
)
上記の例と同じことを純粋なJSで再現してみましょう。
const appendHelloWorld = () => {
const hello = document.createElement('p')
const textNode = document.createTextNode("Hello, World!")
hello.appendChild(textNode)
document.body.appendChild(hello, document.getElementById("root"))
}
appendHelloWorld()
appendHelloWorld()
appendHelloWorld()
どうでしょう?純粋なJSでは API をいくつか呼んで処理を実装する必要がありましたが、JSXではJSの変数に簡単なHTMLを代入したものを呼ぶだけで済んでいますね。
もっと詳しく見てみましょう。
例えば、要素にクラスやスタイルなどの属性を指定したいとき。
JSXだったら非常に簡単です。
const hello = <p style={{ color: "red" }}>Hello, World!</p>
ReactDOM.render(
<>
{hello}
{hello}
{hello}
</>,
document.getElementById("root")
)
通常のHTMLにstyleを定義するような感覚で指定することができていますね。
では、これを純粋なJSで書いてみましょう。
const appendHelloWorld = () => {
const hello = document.createElement('p')
hello.setAttribute("style", "color: red")
hello.appendChild(document.createTextNode("Hello, World!"))
document.body.appendChild(hello, document.getElementById("root"))
}
appendHelloWorld()
appendHelloWorld()
appendHelloWorld()
もうすでに嫌な予感がしますね。スタイルの調整はJSを知る人以外の方にとって扱いづらくなってしまっています。
更に、 World
をユーザーの名前にしたいという要望が出てきたとしましょう。
まずはJSXから。
const Hello = (props) => {
return <p>Hello, {props.name}!</p>
}
ReactDOM.render(
<>
<Hello name="Taro" />
<Hello name="Hanako" />
<Hello name="Kyosuke" />
</>,
document.getElementById("root")
)
では、純粋なJSならどうなるでしょうか。
const appendHelloWorld = (name) => {
const hello = document.createElement('p')
const textNode = document.createTextNode(`Hello, ${name}!`)
hello.appendChild(textNode)
document.body.appendChild(hello, document.getElementById("root"))
}
appendHelloWorld('Taro')
appendHelloWorld('Hanako')
appendHelloWorld('Kyosuke')
どうでしょう?JSXは非常に簡単ですね。今後ロジックが増えてきたときにどちらに保守性において軍配が上がるかも容易に想像できると思います。
上記、非常に簡単な例ではありますが、JSXを使えば単純なJSとHTMLから逸脱せずに直感的に実装しやすいことがお分かりいただけたでしょうか。
JSXについて、Reactの公式ドキュメントが参考になるのでに興味が出てきた方はぜひ読んでみてください。
Reactは使いたいけどJSXは絶対イヤ!って方へ
Vueとの比較
Typescript との相性
ReactもVueもTypescriptに対応しています。プロジェクトが大きくなるにつれて複雑な処理が増える分、型安全であることでバグの混入を防ぐことができます。弊社でもTypescriptを導入することはスタンダードになりつつあり、ReactやVueを採用したプロジェクトでは基本的にTypescriptも採用しています。
そのため、Typescriptも採用するつもりだけど、Typescript入れるならどっちが良いの?逆になんか困ることあるの? と気になっている方も多いでしょう。
僕の場合、VueでTypescriptを使うと時々ストレスを感じることがあるのに対して、ReactではストレスフリーにTypescriptを活用できる点もReactが楽しい理由の1つでした。
Vue × Typescript の課題
では Vue × Typescript で実際にどういうところでストレスを感じたのか、例を見ていきましょう。
(くどいようですが、 Vue v2 時点の話であることを念頭に置いておいてください。)
1. 未定義の変数やコンポーネントを検出してくれない
例えば、Vueにおいて外部のコンポーネントを子コンポーネントとして使いたい場合は
<template>
<Fuga />
</template>
<script lang='ts'>
import Vue from 'vue'
import Fuga from './Fuga.vue' // 必須
export default Vue.extend({
name: 'Hoge',
components: {
Fuga // 必須
}
})
</script>
</script>
のように import して components に値として渡す必要がありますが
<template>
<Fuga />
</template>
<script lang='ts'>
import Vue from 'vue'
export default Vue.extend({
name: 'Hoge'
})
</script>
のように import 諸々の処理を忘れていても、なんとコンパイルエラーになりません。動かして初めてバグに気づくJSのままです。
Reactは検出してくれます。
const Hoge = () => {
return <Fuga /> // Cannot find name 'Fuga'.ts(2304)
}
2. propsの型定義にひとクセあり
例えば、Vue( SFC )で、nullableなstring型であるpropsを定義すると以下になります。
import Vue, { PropType } from 'vue'
export default Vue.extend({
name: 'Hoge',
props: {
fuga: {
type: String as PropType<string | null>, // クセ
default: null
}
}
})
コンストラクタである String
を型に置いて PropType
なるものを import してきて as
でアサーションするなど、ちょっとクセがありますよね。この文法に慣れるまでに何度「あれ、どうだったっけ?」と立ち戻ったことでしょう。。
では、Reactではどうなるかというと、
interface Props {
fuga: string | null
}
const Hoge = (props: Props) => {
...
}
となります。
Reactだからといって特に変わったところはなく、シンプルにTypescriptそのもので書けていますよね。
3. propsのデフォルト値の指定にもひとクセ
Object型またはArray型のpropsのデフォルト値を指定するときもひとクセあるので見てほしい。
Vueはこうなります。
import Vue, { PropType } from 'vue'
interface FugaObj {
field?: string
}
export default Vue.extend({
name: 'Hoge',
props: {
fugaArray: {
type: Array as PropType<string[]>,
default: (): string[] => [] // クセ
},
fugaObj: {
type: Object as PropType,
default: () => ({}) // クセ
}
}
})
Reactならこうです。
interface FugaObj {
field?: string
}
interface Props {
fugaArray: string[]
fugaObj: FugaObj
}
const Hoge = ({ fugaArray = [], fugaObj = {} }: Props) => {
...
}
Vueでは関数でデフォルト値を返す必要があるのに対して、Reactは単純な実装で済んでいますね。
4. 型定義がスルーされてしまう場合がある
例えば、
import Vue, { PropType } from 'vue'
export default Vue.extend({
name: 'Hoge',
props: {
fuga: {
type: String,
default: null
}
},
computed: {
fugaLength () {
return this.fuga.length // Uncaught TypeError: Cannot read property 'length' of null
}
}
})
とした場合に fuga
のデフォルト値から分かるように nullable なので、正しくは型定義に String as PropType<string | null>
と指定する必要があります。
しかし、コンパイルエラーは起こらずスルーされてしまいます。。
5. 型推論が効かない場合がある
例えば、template
ブロック内では型推論が効かないため、うっかり
<template>
<div>
<p>{{ fuga }}</p>
<p>文字列の長さは、 {{ fugaLength.length }} です。</p> <!-- うっかり\(^o^)/ -->
</div>
</template>
<script lang='ts'>
import Vue from 'vue'
export default Vue.extend({
name: 'Hoge',
props: {
fuga: {
type: String,
default: 'hello'
}
},
computed: {
fugaLength (): number {
return this.fuga.length
}
}
})
</script>
としたとしても fugaLength.length
はコンパイルエラーにはなりません。。
これが派生して、以下のように、期待されている型でない値を子コンポーネントのpropsに渡してしまっているときに特に困ったりします。
// string型 を想定している props に number型 を渡してもコンパイルエラーにならない
<template>
<ChildComponent
:fugaString="fugaNumber"
/>
</template>
うーん、Typescriptの意味がないですね。。
ESLint との相性
JSで開発するにあたりESLintは欠かせないですよね。
ReactでESLint関連でストレスを感じた記憶はありませんが、Vueでは一悶着あったのでご紹介します。
Vue × ESLint の課題
React(JSX)では特に拡張機能を追加インストールする必要もなく、始まりのタグを書いたら閉じタグを自動で挿入してくれる恩恵に授かることができました。
一方、Vueでは Vetur という拡張機能をインストールするのが主流ですが、これをインストールしても閉じタグ自動挿入の恩恵は与えられませんでした。なので、Vueを使い始めた当時、閉じタグがめんどくさいからという理由で pug を導入していたのですが、結果的に pug を使うことはやめて純粋な html に戻すことになりました。それまでに作っていたコンポーネントをすべてリプレイスです。。
pug をやめた理由は、詳しくは避けますが、pug の場合 ESLint がまったく動かないというわけではないですが 痒いところに手が届かないという問題 があったためです。
例えば、ESLintでインデントを揃えたり未使用の変数やコンポーネントを検出してゴミコードに気づきやすくしたりすると思いますが、それらがうまく機能しませんでした。コンポーネントが増えたりコンポーネントのロジックが増えてくるとこれがボディブローのように効いてきます。。
.vueファイルでESLintを実行可能にするライブラリである vue-eslint-parser
にpug対応の issue が上がっていますが更新は止まってしまっています。
そのため、ESLintの導入を考えている場合、Vueではtemplateでpugなどのテンプレートエンジンを使うことはおすすめしません。(少なくともpugは。)
しかし、pug をやめて純粋な html に戻したとしても、十分にESLintの恩恵を受けているとは言い切れない状態です。なぜなら、前述のTSとの相性で説明したのと同様に、template部分で使っている未定義の変数やコンポーネントに対してエディタ上のESLintで検出することができないからです。
その他細かいこと
子要素に要素を渡す難易度
子要素に要素を渡すとは、例えばReactではこういうことです。
const Wrapper = ({ children }) => {
return (
<div>
{ children }
</div>
)
}
const Hoge = () => {
return (
<Wrapper>
<p>Hello, World!</p>
</Wrapper>
)
}
この場合、 <p>Hello, World!</p>
は Wrapperコンポーネントの children
に渡されます。
同じことをVueでやってみましょう。(script省略)
// Wrapper.vue
<div>
<slot />
</div>
// Hoge.vue
<template>
<Wrapper>
<p>Hello, World!</p>
</Wrapper>
</template>
<p>Hello, World!</p>
は Wrapperコンポーネントの <slot />
に渡されます。
大差ない気がしますね。
では複数の要素を渡したいときはどうなるでしょうか?
Reactはこうなります。
const Wrapper = ({ first, last }) => {
return (
<div>
{ first }
{ last }
</div>
)
}
const Hoge = () => {
const first = <p>1</p>
const last = <p>2</p>
return (
<Wrapper
first={ first }
last={ last }
/>
)
Vueはこう。
// Wrapper.vue
<template>
<div>
<slot name="first" />
<slot name="last" />
</div>
</template>
// Hoge.vue
<template>
<Wrapper>
<template v-slot:first>
<p>1</p>
</template>
<template v-slot:last>
<p>2</p>
</template>
</Wrapper>
</template>
途端に明暗くっきりですね。
Reactの方がJSの力を借りて工夫して読みやすくする余地がありますが、Vueではこれ以上はどうにもならないかもしれません。また、Reactは要素をpropsとして渡すことができているので、Vueのような独特なシンタックスを気にすることなくほぼ直感的に実装することが可能ですね。
詳しくは、それぞれ以下をご参考ください。
- React: https://ja.reactjs.org/docs/composition-vs-inheritance.html
- Vue: https://jp.vuejs.org/v2/guide/components-slots.html
おわりに
随分React贔屓な記事となってしまいましたが、断じてVueが嫌いなわけではありません。
Vueのプロジェクトに参加してからReactのプロジェクトに参加したので、Reactの便利さがVueの不満点を解消していたことでより魅力的に見えたのだと思います。また、エアプにならないように言及を避けてきましたが、Vue は v3 にアップデートされたことで、僕がここで書いた内容が克服されている可能性もありますし、Reactよりも有利な点もあるだろうと思います。
Vue v3 を使う機会に恵まれたら Vue 贔屓の記事を書けたら書こうと思います。
ReactかVueかで悩んでいる方の一助になれば幸いです!
ありがとうございました。
宣伝
そんな React で楽しく開発した DocBase は社内・社外の垣根を超えて情報共有ができる情報共有サービスです。
(もちろん大変なこともたくさんありましたが)
細かい権限設定ができるので、オープンな情報もクローズドな情報も DocBase に集約できます。そして大きな企業さまでも全員で使えるよう、セキュリティには最も力を入れています。
Reactに改修後、高速化にも取り組んでいるので情報共有サービスの中でもサクサク使えるほうだと思います。
チームを作成して30日間は無料ですべての機能が使えるので、ぜひチームを作成して試してみてください。
このエントリーに対するコメント
日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)
- トラックバック
「いいね!」で応援よろしくお願いします!