34
29

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ReactエンジニアがVueでも同じことできるやろ、と言われた時のお話

Posted at

はじめに

今回はVue+NuxtのWebアプリケーションを実装していきます。筆者の今までの経験としてはTypescript×ReactでのWebアプリケーションの経験が多いため、ハンズオンというよりかはReactとの違いを理解しながら、Vueコードを理解できるようにするための学習&備忘録を目的としています。
⚠️どちらも中級レベルの理解度ではあるので、考え方や設計思想などもし間違っている点ありましたらご指摘いただけると幸いです。

前提知識:

  • HTML/CSS/JavaScript/Reactの基礎知識

Vueの基本構文

単一ファイルコンポーネント(SFC)

Vueファイルは以下の3つのセクションで構成されます:

  • <template>: HTML記述部分
  • <script>: JavaScript記述部分
  • <style>: CSS記述部分

この3つを一つのファイルで管理するコンポーネントをSFC(Single File Component:単一ファイルコンポーネント) と呼びます。

1例として最小限シンプルなサンプルコードです。3つを1ファイルにまとまって定義されているのがわかります。

<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>

<template>
  <button @click="count++">Count is: {{ count }}</button>
</template>

<style scoped>
button {
  font-weight: bold;
}
</style>

またscript内のコードは、Typescriptで書くこともできます。

<script lang="ts">
// TypeScriptのコードをここに記述
</script>

Composition APIとscript setup

Vue 3では<script setup>構文を使用してComposition APIを簡潔に記述できます。
Composition APIを使用することで、リアクティブな値や関連する関数をコンポーネントから切り離して扱えるようになります。これにより、コードの再利用性や保守性が向上します。

<script setup lang="ts">
    import { ref } from 'vue'
    
    const count = ref(0)
    const increment = () => {
      count.value++ 
    }
</script>

<script setup>とは

Vue 3で導入された糖衣構文(シンタックスシュガー)です。
従来のComposition APIではdefineComponentでコンポーネントを定義し、setup()関数内で変数や関数を定義した上でreturnする必要がありました。

<script setup>を使うことで:

  • defineComponentの記述が不要
  • returnでで利用するすべてを返す必要がない
  • トップレベルに書いた変数・関数がそのままtemplateで使える

Reactの関数コンポーネントに近い書き心地となります。

☆Reactとの実行タイミングの違い:リアクティブシステム

Reactでは再レンダリング(stateの変更やpropsの変更)のたびに関数コンポーネント全体が実行されますが、Vueの<script setup>内のコードはマウント時に1回だけ実行されます。

<Reactの場合>

react
const Counter = () => {
  const [count, setCount] = useState(0)
  
  // setCountが呼ばれると...
  // → コンポーネント関数全体が再実行される
  // → 新しいJSXが生成される
  // → 仮想DOMの差分比較(diffing)
  // → 差分だけ実DOMに反映
  
  return <div>{count}</div>
}

setter関数の呼び出しで再レンダリングが発生し、コンポーネント関数全体を再実行して結果を比較します。

<Vueの場合>

vue
<script setup lang="ts">
import { ref } from 'vue'

const count = ref(0)
const increment = () => {
  count.value = 1 // script内では .value が必要
}

// count.value = 1 が実行されると...
// → Vueが「countが変わった」と検知
// → countを使っているDOM部分だけ特定
// → その部分だけ更新
</script>

<template>
 <!-- templateでは .value 不要! -->
  <div>{{ count }}</div>
</template>

refのsetterで変更を追跡し、変更された値を使っている更新に必要なDOM部分だけを更新します。
→そのため、ReactのようにuseCallbackで関数の再生成を防ぐといった最適化は基本的に不要です。

なお、<template>内では.valueは不要です。Vueのコンパイラが自動的にアンラップ(.valueを付与)してくれます。

単方向データバインディング

データバインディングとはデータの更新をDOMの更新に反映させる仕組みです。

まず初めに、JavaScriptのデータが変更→DOMを変更方向のデータバインディング機能をご紹介

マスタッシュ構文

{{ }}(マスタッシュ構文)を使用することで、JavaScriptの変数や関数の結果をHTMLに展開できます。

<div>{{ errorText }}</div>
<div>{{ getErrorText(code) }}</div>
<div>{{ 1 + 1 }}</div>  <!-- 2 と表示される -->

v-bind ディレクティブ

v-bindを使用すると、HTML要素の属性に動的な値をバインドできます。省略記法として:が使えます。

<input v-bind:value="message">
<input :value="message">

ディレクティブとはv-から始まる属性のことで、Vueの機能をHTML要素に適用するための仕組みです。directive(指令)を意味します。


続いてDOMが変更→JavaScriptのデータを変更方向のデータバインディング機能をご紹介

v-on ディレクティブ

DOMイベントを監視し、イベント発生時に処理を実行できます。省略記法として@が使えます。

<button v-on:click="onClick" >
<button @click="onClick" >

監視できるイベントは様々あり、上記ではクリックイベント(click)を出しましたが、他にも入力イベント(input)や送信イベント(submit)などです。それぞれに修飾子をつけることによって、特定のイベントに制限することも可能である。

参照:Vue.jsイベントハンドラー(イベント名)一覧

双方向データバインディング

v-model ディレクティブ

v-modelは、入力要素とデータを双方向にバインドします。ユーザーの入力が即座にデータ反映され、データの変更も即座に反映されます。

<input type="text" v-model="message">
<input type="checkbox" v-model="isSelected">

☆Reactとのデータフローの違い

双方向データバインディングについて、Reactは単方向データフローを重視しており、データの流れを明示的に記載しますが、Vueは双方向データバインディングを便利機能として提供しています。

<Reactの場合>

react
<input 
  type="text"
  value={message}                              // データ → UI
  onChange={(e) => setMessage(e.target.value)} // UI → データ
/>

<Vueの場合>

vue
<!-- Vue: v-model で双方向バインディングが1行で完結 -->
<input type="text" v-model="message">

v-model:value + @inputの糖衣構文(シンタックスシュガー)なので、展開して明示的に書くこともできます。

条件付きレンダリング

v-if ディレクティブ

条件に応じて要素を表示/非表示にします。条件がfalseの場合、要素はDOMから完全に削除されます。

<h1 v-if="awesome">Vue is awesome!</h1>
<h1 v-else>Oh no</h1>

複数の要素をIf文で切り替えたい場合は<template>を利用します。

<template v-if="awesome">
  <p>Paragraph 1</p>
  <p>Paragraph 2</p>
</template>

v-show

条件に応じて要素を表示/非表示にします。要素は常にDOMに存在し、CSSのdisplayプロパティで制御します。

<h1 v-show="awesome">Vue is awesome!</h1>

複数の要素をshow文で切り替えたい場合は<template>を利用します。

<template v-show="awesome">
  <p>Paragraph 1</p>
  <p>Paragraph 2</p>
</template>

v-ifv-showの使い分け

v-if v-show
動作 DOMから完全に削除/追加 display: noneで非表示にするだけ
初期コスト 低い(falseなら何もレンダリングしない) 高い(常にレンダリングされる)
切り替えコスト 高い(DOM要素を作り直す) 低い(CSSを切り替えるだけ)
使い分け 権限分岐などによる表示切り替えのもの タブやトグルなど頻繁に切り替わるもの

☆Reactの条件分岐実装との違い:条件付きレンダリング

Reactの三項演算子や&&演算子は、Vueのv-ifに相当します。上記紹介でのVue実装例をReactで記載するとこのようなイメージ

react
// React: DOMから削除される
{awesome && Vue is awesome!} // v-if
{awesome ? Vue is awesome! : Oh no} // v-if / v-else

一方、Vueのv-showに相当する標準機能はReactにはありません。同じことをするには自分でstyleを書く必要があります。

react
// React: 自分でstyleを書く
<h1 style={{ display: awesome ? 'block' : 'none' }}>Vue is awesome!</h1>

☆ReactのJSXとの違い:マスタッシュ構文

ReactのJSXでは{}の中にJSX要素を含めることができます。ReactはJavaScriptの中にHTMLを書くという考え方になります。

react
// React: {} の中でJSX要素を返せる
  {items.map(item => {item})}

一方、Vueのマスタッシュ構文{{}}テキスト出力専用です。HTMLタグを書いてもエスケープされて文字列として表示されます。そのため、VueはHTMLの中にディレクティブで制御を加えるという考え方になります。

vue
<!-- Vue: {{ }} はテキストのみ -->
{{ "<strong>太字</strong>" }}  <!-- そのまま文字列で表示される -->

Vueで配列のループや条件分岐をしたい場合は、v-forv-ifなどのディレクティブを使います。

vue
<!-- Vue: ディレクティブでループ -->
<ul>
  <li v-for="item in items" :key="item">{{ item }}</li>
</ul>

リストレンダリング

v-for ディレクティブ

配列やオブジェクトの要素を繰り返しレンダリングできます。

<li v-for="item in items" :key="item.id">
  {{ item.name }}
</li>

複数の要素をFor文でレンダリングしたいは<template>を利用します。

<template v-for="item in items" :key="item.id">
    <h3>{{ item.title }}</h3>
    <p>{{ item.description }}</p>
  </template>

コンポーネント間通信(Props と Emit)

Reactでは親子間のデータ受け渡しは全てPropsで行いますが、VueではPropsEmitで役割を分けています。

Props(親→子へのデータ渡し)

親コンポーネントから子コンポーネントに対して値を渡します。

Propsの宣言にはdefinePropsマクロを利用します。<script setup>内で使用でき、importなしで使える関数です。デフォルト値を設定したい場合はwithDefaults(defineProps<{ 型定義 }>(),{ デフォルト値 })を使います。

Emit(子→親へのイベント通知)

子コンポーネントでボタンが押されたり、テキストボックスの変更などが発生したときに、子コンポーネント→親コンポーネントに伝えることができます。

また、Propsの値は子から直接変更できないため、子での操作をトリガーに親で変更処理を行います。

Emitの宣言にはdefineEmitsマクロを利用します。<script setup>内で使用でき、importなしで使える関数です。

利用例

<PropsEmitDemo
  :count="count"           <!-- 子へデータを渡している -->
  @count-up="handleCountUp"  <!-- 子→親へイベントを受け取っている -->
/>

親コンポーネント

<script setup lang="ts">
  import { ref } from 'vue'
  import PropsFunctionDemo from '~/PropsFunctionDemo.vue'

  // 状態
  const count = ref(0)

  // イベントハンドラ
  const handleCountUp = () => {
    count.value++
  }
</script>

<template>
  <div>
    <h1>親: {{ count }}</h1>
    <PropsFunctionDemo :count="count" @count-up="handleCountUp" />
  </div>
</template>

子コンポーネント

<script setup lang="ts">
  // Propsの定義(受け取るデータ)
  defineProps<{
    count: number
  }>()

  // Emitの定義(発火するイベント)
  const emit = defineEmits<{
    'count-up': []
  }>()

  // ボタンクリック時
  const onClick = () => {
    emit('count-up') // 親に「count-upイベントが起きた」と通知するだけ
  }
</script>

<template>
  <div>
    <p>子で表示: {{ count }}</p>
    <button @click="onClick">+1</button>
  </div>
</template>

emitの呼び出し方について
Vue のテンプレート内では、式として直接 emit() を実行しないように注意します。イベントハンドラには「関数参照」または「関数を返す式」を渡します。

// NG例(レンダリング時に実行)
@click="emit('click')"

// OK例(関数実行)
@click="() => emit('click')"  
@click="handleClick"

React の onClick={handleClick} と同様に、Vue でも「関数を渡す」イメージです。

☆Reactとの書き方の違い

Reactの場合:全てPropsで親→子へ

react
<Counter count={count} onCountUp={handleCountUp} />

Vueの場合:役割ごとにPropsとEmitの使い分け

vue
<Counter :count="count" @count-up="handleCountUp" />

Reactでは全てPropsにしていたところを、VueだとPropsとEmitに分ける形になります。
EmitはイベントといってもReactでonHogeHogeにしていたものの代わりに使うイメージで問題ありません。

UIライブラリ

VuetifyはVueアプリケーション向けのUIライブラリで、GoogleのMaterial Designに基づいたコンポーネントが多数提供されています。

☆Reactで言うMUIやChakra UIに相当するイメージです。

Nuxt

NuxtはVue向けのフルスタックJavaScriptフレームワークです。
☆Reactで言うNext.jsに相当します。

もちろんVue単体(Vite + Vue)でも開発できますが、Nuxtを使うことで以下の機能が標準で提供されます

  • ファイルベースルーティング: pages/ディレクトリにファイルを置くだけでルーティングが自動生成
  • SSR/SSG: サーバーサイドレンダリング・静的サイト生成が標準サポート
  • API Routes: server/api/でバックエンドAPIを同一プロジェクト内に記述可能

ルーティング(pages/)

Nuxtではファイルベースルーティングが標準で組み込まれています。

pages/index.vue →/
pages/users.vue →/users
pages/users/[id].vue →/users/{id}

有効にするには、以下のように定義します。

App.vue
<template>
  <NuxtPage />
</template>

レイアウト(layouts/)

全てのページに共通するUIコンポーネント(ヘッダー、フッターなど)や、複数ページで使用するレイアウトを定義します。

有効にするには、以下のように定義する

App.vue
<template>
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>

利用するページでは、以下のように定義する

index.vue
<script setup lang="ts">
definePageMeta({
  layout: 'default', // Layoutファイル名
})
</script>

☆Next.jsのApp Routerとの比較

Nextは、ページはpages/配下、レイアウトはlayout/配下のように、役割ごとにディレクトリを分ける構成です。

一方Next.jsのApp Routerはルート(機能)ごとに関連ファイルをまとめる構成です。

app/users/[id]/
├── page.tsx      ← ページ本体
├── layout.tsx    ← レイアウト
└── loading.tsx   ← ローディング

Composables(composables/)

再利用可能なロジックや状態を定義します。複数のコンポーネント間でロジックや変数を共有する際に使用します。

refを使用すると、リアクティブな状態となり値の変更を自動的に検知し、それに応じてDOMを更新できます

vue
export const useStatus = () => {
  const publishStatus = ref('PRIVATE')
  const selectedGrades = ref([])
  
  return { publishStatus, selectedGrades }
}

☆ReactのCustom Hooksとの比較

ReactのCustom Hooksは呼び出すたびに独立したstateが作られます。そのため、Hook内でuseStateを使っても、それはローカルstateであり、コンポーネント間で自動的に共有されることはありません。

react
// React: 外に書いてもリアクティブにならない
const count = 0  // ただの変数(再レンダリングされない)

export const useCounter = (initialValue = 0) => {
  const [count, setCount] = useState(initialValue)
  
  const increment = useCallback(() => setCount(prev => prev + 1), [])
  const decrement = useCallback(() => setCount(prev => prev - 1), [])
  
  return { count, increment, decrement }
}
react
// useCounterを複数回使うと、それぞれ独立したstateになり、値は共有されません。
const A = () => {
  const { count } = useCounter()
}

const B = () => {
  const { count } = useCounter()
}

Reactでstateを共有したい場合は、Jotai(Atom)やRecoil、Redux、Context APIなどstateをReactの管理下に置いたまま共有する仕組みが必要になります。

react
// React: 外に書いてもリアクティブにならない
const count = 0  // ただの変数

// グローバルstateにはJotai等が必要
import { atom, useAtom } from 'jotai'
const countAtom = atom(0)

const Component = () => {
  const [count, setCount] = useAtom(countAtom)
}

一方、VueのComposablesでは、ref自体がリアクティブなオブジェクトのため、refをどこで定義するかによって、stateが共有されるかどうかが決まります。

関数内でref定義(独立したstate)

vue
export const useCounter = (initialValue = 0) => {
  const count = ref(initialValue) // 呼び出しごとに新しいref

  const increment = () => count.value++
  const decrement = () => count.value--

  return { count, increment, decrement }
}
vue
// 呼び出しごとに独立したstateになる
const a = useCounter()
const b = useCounter()

モジュールスコープでref定義(共有されるstate)

vue
// モジュールスコープでrefを定義
const count = ref(0)

export const useCounter = () => {
  const increment = () => count.value++
  const decrement = () => count.value--

  return { count, increment, decrement }
}

Vueのrefはそれ自体がリアクティブなオブジェクトなので、モジュールレベルで宣言するだけでページ遷移しても保持されるグローバルstateになります。追加のライブラリは不要です。

まとめ

今回はReact目線からVue/Nuxtの基礎を学びました。Vueの思想に戸惑いつつも、1つの言語学習だけではわからなかった考え方を学び、それぞれで使いやすところ、好きなところを見つけられる良い機会だったなと思いました。
どちらの言語でもより使いこなせるようこれからも頑張っていきます〜!

蛇足

糖衣構文(シンタックスシュガー)という言葉。簡略された記載方法だよ、という意味なのだが個人的になーんでそんな言葉になったのだろうと不思議な思い。
Jacvascriptの配列宣言やアロー関数など言葉を知らなくても、多くの方が利用しているかつ理解できるのですが突然出てくると、独自の構文かしらの思ってしまう不思議な言葉。

語源は、

「取り扱いやすい」を意味するsweetの第一義が「(砂糖のように)甘い」であることから。

だそう。

...コードを記載する際にも理解しやすいを常に心がけたいですね。ニコ

参照:英辞郎ontheweb:syntactic sugarとは

34
29
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
34
29

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?