【2024年版】VueとReactのちょっとした比較

筆が遅くて年が明けました。
今回の実験のソースは [こちら](https://github.com/rmrf12071/2024-react-vue-test)。

## おことわり

個人的にVueの方が好きなのでVue寄りです。

## 各バージョン

|フレームワーク|フレームワークのバージョン|TypeScriptのバージョン|ルーティングライブラリ|ルーティングライブラリのバージョン|
|---|---|---|---|---|
|Vue.js|3.5.13|5.6.3|Vue Router|4.5.0|
|React|18.3.1|5.6.3|React Router|7.1.1|

## プロジェクトの準備

### Vue.js

[公式ドキュメントのクイックスタート](https://ja.vuejs.org/guide/quick-start.html) の通りです。

```bash
npm create vue@latest
Need to install the following packages:
create-vue@3.13.0
Ok to proceed? (y) y


> npx
> create-vue


Vue.js - The Progressive JavaScript Framework

√ Project name: ... vue-test
√ Add TypeScript? ... No / Yes
√ Add JSX Support? ... No / Yes
√ Add Vue Router for Single Page Application development? ... No / Yes
√ Add Pinia for state management? ... No / Yes
√ Add Vitest for Unit Testing? ... No / Yes
√ Add an End-to-End Testing Solution? » No
√ Add ESLint for code quality? » Yes
√ Add Prettier for code formatting? ... No / Yes

Scaffolding project in 2024-react-vue-test/vue-test...

Done. Now run:

  cd vue-test
  npm install
  npm run format
  npm run dev
```

### React

ルーターのライブラリはいろいろあるようですが、今回は`React Router`を使います。
`Framework`モードと`Library`モードの2つのつくりかたがあるようですが、今までと同じようなかたちで使える`Library`モードから作成してみます。
公式ドキュメントは [こちら](https://reactrouter.com/start/library/installation)。

```bash
npx create-vite@latest && cd react-test && npm i react-router
Need to install the following packages:
create-vite@6.1.1
Ok to proceed? (y) y

√ Project name: ... react-test
√ Select a framework: » React
√ Select a variant: » TypeScript + SWC

Scaffolding project in 2024-react-vue-test/react-test...

Done. Now run:

  cd react-test
  npm install
  npm run dev


added 147 packages, and audited 148 packages in 19s

40 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
```

それぞれのプロジェクトが準備できました。比較していきます。

## 比較1: 構文

なんといっても、書き方がだいぶ異なります。(Vue側がReactに近い書き方をできなくもないですが)
以下は、プロジェクト作成時に存在するファイルの抜粋です。

まずは、Vueから。

```vue:src/App.vue
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
import HelloWorld from './components/HelloWorld.vue'
</script>

<template>
  <header>
    <img alt="Vue logo" class="logo" src="@/assets/logo.svg" width="125" height="125" />

    <div class="wrapper">
      <HelloWorld msg="You did it!" />

      <nav>
        <RouterLink to="/">Home</RouterLink>
        <RouterLink to="/about">About</RouterLink>
      </nav>
    </div>
  </header>

  <RouterView />
</template>

<style scoped>
header {
  line-height: 1.5;
  max-height: 100vh;
}

/* 以下略 */
</style>
```

次に、React。

```react:src/App.tsx
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'

function App() {
  const [count, setCount] = useState(0)

  return (
    <>
      <div>
        <a href="https://vite.dev" target="_blank">
          <img src={viteLogo} className="logo" alt="Vite logo" />
        </a>
        <a href="https://react.dev" target="_blank">
          <img src={reactLogo} className="logo react" alt="React logo" />
        </a>
      </div>
      <h1>Vite + React</h1>
      <div className="card">
        <button onClick={() => setCount((count) => count + 1)}>
          count is {count}
        </button>
        <p>
          Edit <code>src/App.tsx</code> and save to test HMR
        </p>
      </div>
      <p className="read-the-docs">
        Click on the Vite and React logos to learn more
      </p>
    </>
  )
}

export default App
```

大きな違いは、Vueの方が「SFC」(単一ファイルコンポーネント)になっているところです。
詳細は [公式](https://ja.vuejs.org/guide/scaling-up/sfc.html) をご覧いただきたいですが、Vueで私がいちばん推したいのはここ(特にscoped css)です。
Reactの方は、「コンポーネント内容(JSX)を返すただの関数」になっています。そのため、１ファイル内に複数のコンポーネントを記述することができます。

この、「VueのSFC」と「Reactの関数コンポーネント」に起因する主な違いをまとめます。
Reactの「コンポーネントに閉じたCSS」を△としているのは、class属性値が自動生成されてWebインスペクターで探しにくくもやっとすることが多いからです。(ブラウザ拡張機能を使え、ということでしょうが...)

|フレームワーク|拡張子|1ファイル中のコンポーネント数|コンポーネントに閉じたCSS|
|---|---|---|---|
|Vue(SFC)|vue|1|◎|
|React|tsx|0以上|△(CSS-in-JS系ライブラリ利用)|

Vueの方の書き方がちょっと特殊で自由度もReactより下がりますが、チームで開発するうえではその分「トリッキーなことをしにくい」ので一貫性を保ちやすい利点があると考えています。
ReactはVueのように「SFCのJSへのビルド」がない分関数に書いてあるように動きます。「事実上の双方向バインディング」と複数の`useEffect`により地獄を見ることがままあります。
個人的なイメージとしては、「Vueが高水準、Reactが低水準」です。

## 比較2: レンダリング/ライフサイクル

話を進めるためにはココを避けて通れません。
ReactもVueも、ざっくりいうと「リアクティブ値が変わる → 描画(レンダリング)する」という流れは変わりませんが、再描画時に動作する範囲が異なります。

Reactは、「ぜんぶ」(関数まるごと)動きます。再描画時に新しく関数が呼び出されるため、前回の値を関数外にキャッシュする方法が必要で、それを`useState`等が担います。この「まるごと」というところに直感と反するところがあり、慣れるのに時間が必要ということから「難易度が高い」といわれるのだと思います。
Vueは、基本的に`template`タグの中身だけ呼び出されます。`script`タグの中身は、ライフサイクルイベントや算出プロパティくらいしか呼び出されません。Reactと比較すれば直感的だと思います。(慣れは必要ですが)

文字だと伝えにくいですが、再描画時に「Reactはまるごと、Vueは一部」動くということです。
後述するルーティングライブラリの仕様がこのフレームワーク自体の仕様によるものだと思われます。

## 比較3: ルーティングとそのブロック

ブロックというのは、「やっぱり移動キャンセル！」というやつです。
`Vue Router`では`onBeforeRouteLeave`(パス移動先が別コンポーネント)と`onBeforeRouteUpdate`(パス移動先が同一コンポーネント)、`React Router`では`useBlocker`で移動のキャンセルができます。

まず、Vue の方のソースはこちら。( GitHub上のソースは tsx の実験をしたのでもう少し多いです )
下記にあるように、その気になれば引数に「非同期関数」を指定できます。  
ブラウザバックを連打すると、非同期処理が終わる前にまた呼ばれるのでその点は注意ですが、「確認ダイアログをはさむ」といった実装は直感的にできます。

```vue:src/view/TestView.vue
<script setup lang="tsx">
import { computed, ref } from 'vue';
import { onBeforeRouteLeave, onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router';

const router = useRouter();
const route = useRoute();
const count = ref(0);
const countUp = () => count.value++;
const onNav = () => router.push('/');

// trueを返すとブロックできる、falseを返すか何も返さないとブロックしない
onBeforeRouteLeave(async (to, from) => {
  console.log(to, from, count.value);
  await new Promise((resolve) => setTimeout(resolve, 1000));
  return count.value > 10;
});
onBeforeRouteUpdate(async (to) => {
  console.log(to);
  await new Promise((resolve) => setTimeout(resolve, 1000));
  count.value = 0;
});
</script>

<template>
  <div>
    <span>{{ route.params.pathMatch[0] }}</span>

    <button @click="countUp">Click Me</button>
    <button @click="onNav">Click Me (2)</button>
  </div>
</template>

<style lang="css" scoped>
div {
  display: flex;
  flex-direction: column;
}
span {
  color: green;
}
button {
  background-color: yellow;
}
</style>
```

次に、React の方はこちら。
Vueの時と異なり、非同期関数を指定できません。
これは、前のセクションで概要を記載した、「コンポーネントの関数がrenderの度に呼び出される」という React の仕様によるものだと思われます。
ブロックする/しないのフラグが変わると再描画するため、その時には新しい状態でコンポーネントを再定義する必要があります。その際に`useBlocker`へ指定した関数も新しく作成されます。
つまり確認ダイアログを出したいような場合は「とりあえずブロック」して、移動するなら改めて移動、という実装にする必要があります。ちなみにそのブロックする際に、「ブラウザバック」だったのか「ブラウザフォワード」だったのか判定する手段がありません。

```tsx:src/Page.tsx
import { useState } from 'react';
import { NavLink, useBlocker, useParams } from 'react-router';

export default function Page() {
  const { '*': path } = useParams();
  const num = Number(path?.match(/\d+$/));
  const prev = `page${num - 1}`;
  const next = `page${num + 1}`;

  const [max, setMax] = useState(10);

  // 非同期関数は指定できない、「今回のrenderの情報でブロックするか否か」を同期的に返す
  useBlocker(({ nextLocation }) => {
    const nextNum = Number(nextLocation.pathname.match(/\d+$/)?.[0]);
    console.log(nextLocation, nextNum);
    return !isNaN(nextNum) ? nextNum >= max || nextNum <= 0 : false;
  });

  return (
    <div
      style={{
        display: 'flex',
        justifyContent: 'center',
        alignItems: 'center',
        height: '100dvh',
        flexDirection: 'column',
      }}
    >
      <NavLink to={`/${prev}`}>{`move to ${prev}`}</NavLink>
      Now: {path}
      <NavLink to={`/${next}`}>{`move to ${next}`}</NavLink>
      <NavLink to={`/many`}>{`move to many`}</NavLink>
      <div>
        max: <input type="number" value={max} onChange={(e) => setMax(Number(e.target.value))} />
      </div>
    </div>
  );
}
```

## 比較4: 大量コンポーネント

どの程度を「大量」というかは難しいですが、とりあえず1000件の一覧で試してみました。
結果、Vue も React も重くありませんでした。
良い比較の方法があったら教えてください。

Vue のソース

```vue:vues/ManuView.vue
<script setup lang="ts">
import ListItem from '@/components/ListItem.vue';
</script>

<template>
  <ul>
    <li v-for="idx in 1000" :key="idx">
      <ListItem :idx />
    </li>
  </ul>
</template>

<style lang="css" scoped>
li {
  display: flex;
  justify-content: space-between;
}
</style>
```

```vue:src/components/ListItem.vue
<script setup lang="ts">
import { ref } from 'vue';

const { idx } = defineProps<{ idx: number }>();
const checked = ref(false);
</script>

<template>
  <span>{{ `No. ${idx}` }}</span>
  <input type="checkbox" v-model="checked" />
  <input type="checkbox" v-model="checked" />
  <input type="checkbox" v-model="checked" />
</template>
```

React のソース

```tsx:src/Many.tsx
import { useState } from 'react';

export default function Many() {
  const arr = [<ListItem idx={1} key={1} />];
  for (let i = 2; i < 1000; i++) {
    arr.push(<ListItem idx={i} key={i} />);
  }

  return <ul style={{ padding: '10dvh 10dvw' }}>{arr}</ul>;
}

function ListItem(props: { idx: number }) {
  const [checked, setChecked] = useState(false);

  return (
    <li style={{ display: 'flex', justifyContent: 'space-between' }}>
      {`No. ${props.idx}`}
      <input type="checkbox" checked={checked} onChange={() => setChecked(!checked)} />
      <input type="checkbox" checked={checked} onChange={() => setChecked(!checked)} />
      <input type="checkbox" checked={checked} onChange={() => setChecked(!checked)} />
    </li>
  );
}
```


## まとめ

Vue は「ヒトにとって直感的」、React は「(古典)コンピューターにとって直感的」な印象があります。
React の方が人気だと思いますが、人類には早い面もあるので Vue のことも思い出してあげてください！