📝

フロント開発初心者がVueとReactのコードを比較してみた

2023/10/10に公開2

この記事について

  • Reactのフロント開発を1年程度経験した人間が学習目的でVueに触れて、Reactとどのくらい違うのか気になったので検証してみました
  • 技術的な深い部分には触れず、単純なコード量や書きやすさなどを比較しています
  • 自分と同じく、フロントをこれから触れる人へ参考程度になれば幸いです

使用したVueとReactのバージョンはこちら

Vue 3.2.13
React 18.2.0

比較したコード

https://ja.vuejs.org/tutorial/#step-5

  • inputやbuttonのイベント処理、Apiの実行など基本的な機能が揃っているとよいかと思い、Vue学習用に進めていた公式チュートリアルのSTEP5〜STEP10の内容を、1つの画面にまとめVueとReactそれぞれで作成しました

作成した画面

Vueのコード

CompositionApiで作成しています

/Sample.vue
<script setup>
import { computed, ref, onMounted, watch } from "vue";

const text = ref("");
const awesome = ref(true);
let id = 0;
const newTodo = ref("");
const todos = ref([
  { id: id++, text: "Learn HTML", done: true },
  { id: id++, text: "Learn JavaScript", done: true },
  { id: id++, text: "Learn Vue", done: false },
]);
const hideCompleted = ref(false);
const filteredTodos = computed(() =>
  hideCompleted.value ? todos.value.filter((t) => !t.done) : todos.value
);
const pElementRef = ref(null);
const todoId = ref(1);
const todoData = ref(null);

function onInput(e) {
  text.value = e.target.value;
}

function toggle() {
  awesome.value = !awesome.value;
}

function addTodo() {
  todos.value.push({ id: id++, text: newTodo.value, done: false });
  newTodo.value = "";
}

function removeTodo(todo) {
  todos.value = todos.value.filter((t) => t !== todo);
}

onMounted(() => {
  pElementRef.value.textContent = "mounted!";
});

async function fetchData() {
  todoData.value = null;
  const res = await fetch(
    `https://jsonplaceholder.typicode.com/todos/${todoId.value}`
  );
  todoData.value = await res.json();
}

fetchData();

watch(todoId, fetchData);
</script>

<template>
  <h1>Sample</h1>
  <input :value="text" @input="onInput" placeholder="Type here" />
  <p>ここに表示:{{ text }}</p>
  <br />
  <button @click="toggle">toggle</button>
  <h1 v-if="awesome">Vue is awesome!</h1>
  <h1 v-else>Oh no 😢</h1>
  <br />
  <form @submit.prevent="addTodo">
    <input v-model="newTodo" />
    <button>Add Todo</button>
  </form>
  <ul>
    <li v-for="todo in filteredTodos" :key="todo.id">
      <input type="checkbox" v-model="todo.done" />
      <span :class="{ done: todo.done }">{{ todo.text }}</span>
      <button @click="removeTodo(todo)">X</button>
    </li>
  </ul>
  <button @click="hideCompleted = !hideCompleted">
    {{ hideCompleted ? "Show all" : "Hide completed" }}
  </button>
  <br />
  <p ref="pElementRef">hello</p>
  <br />
  <p>Todo id: {{ todoId }}</p>
  <button @click="todoId++">Fetch next todo</button>
  <p v-if="!todoData">Loading...</p>
  <pre v-else>{{ todoData }}</pre>
</template>

Reactのコード

/Sample.jsx

import {useEffect, useRef, useState} from 'react';

export default function Index() {
    const [text, setText] = useState('');
    const [awesome, setAwesome] = useState(true);

    let id = 0;
    let defaultTodos = [
        {id: id++, text: 'Learn HTML', done: true,},
        {id: id++, text: 'Learn JavaScript', done: true,},
        {id: id++, text: 'Learn React', done: false,}
    ];
    const [newTodo, setNewTodo] = useState('');
    const [todos, setTodos] = useState(defaultTodos);
    const [hideCompleted, setHideCompleted] = useState(false);
    const pElementRef = useRef(null);
    const [todoId, setTodoId] = useState(1);
    const [todoData, setTodoData] = useState(null);

    const handleChangeText = (e) => {
        setText(e.target.value);
    }

    const handleClickToggle = () => {
        setAwesome(!awesome);
    }

    const handleOnClickAddTodo = () => {
        setTodos([...todos, {id: id++, text: newTodo, done: false,}]);
        setNewTodo('');
    }

    const handleOnChangeDeleteTodo = (id) => {
        setTodos(todos.filter(todo => todo.id !== id));
    }

    const handleOnChangeCheckTodo = (id) => {
        setTodos(todos.map(todo =>
            todo.id === id ? {...todo, done: !todo.done} : todo
        ));
    }

    const filteredTodos = () => {
        return hideCompleted ? todos.filter(todo => !todo.done) : todos;
    }

    useEffect(() => {
        pElementRef.current.textContent = 'mounted!';
    }, []);

    const fetchTodo = async () => {
        setTodoData(null);
        try {
            const res = await fetch(`https://jsonplaceholder.typicode.com/todos/${todoId}`);
            const data = await res.json();
            setTodoData(data);
        } catch (error) {
            console.error(`error: ${error}`);
        }
    }

    useEffect(() => {
        fetchTodo();
    }, [todoId]);

    return (
        <div>
            <h1>Sample</h1>
            <input type={"text"} placeholder={'Type here'} onChange={handleChangeText}/>
            <p>ここに表示:{text}</p>
            <br />
            <button onClick={handleClickToggle}>toggle</button>
            {awesome ? (<h1>React is awesome!</h1>) : <h1>Oh no 😢</h1>}
            <br />
            <input type={"text"} value={newTodo} onChange={(e) => setNewTodo(e.target.value)} />
            <button onClick={handleOnClickAddTodo}>Add Todo</button>
            <ul>
                {
                    filteredTodos().map((todo) => {
                        return (
                            <li key={todo.id}>
                                <input type="checkbox" checked={todo.done} onChange={() => handleOnChangeCheckTodo(todo.id)} />
                                <span>{todo.text}</span>
                                <button onClick={() => handleOnChangeDeleteTodo(todo.id)}>X</button>
                            </li>
                        );
                    })
                }
            </ul>
            <button onClick={() => setHideCompleted(!hideCompleted)}>{hideCompleted ? 'Show all' : 'Hide completed'}</button>
            <br />
            <p ref={pElementRef}>hello</p>
            <br />
            <p>Todo id: {todoId}</p>
            <button onClick={() => setTodoId(prevId => prevId + 1)}>Fetch next todo</button>
            {!todoData ? (<p>Loading...</p>) : <pre>{JSON.stringify(todoData, null, 2)}</pre>}
        </div>
    );
}

コードで異なる部分を比較してみる

特に印象深く感じた部分をピックアップしてみました

constの定義

  • Vueではconst text = ref("")のようにrefを使用し、text.valueで参照、更新します
  • Reactではconst [text, setText] = useState("")のようにuseStateを使用し、textで参照、setTextで更新します
Vue
const text = ref("");
text.value = e.target.value;
React
const [text, setText] = useState('');
setText(e.target.value);

ライフサイクルフック(画面描画時に一度だけ実行)

  • VueではonMountedを使用します
  • ReactではuseEffectを使用します
Vue
onMounted(() => {
  pElementRef.value.textContent = "mounted!";
});
React
useEffect(() => {
    pElementRef.current.textContent = 'mounted!';
}, []);

依存関係の状態変化

  • Vueではcomputedwatchを使用することで、依存関係の状態変化を検知し、自動的に再計算します
  • Reactでは、computedは同様の処理をfilteredTodos関数を独自に作成、watchはuseEffectを使用して同様に処理しています
Vue
const filteredTodos = computed(() =>
  hideCompleted.value ? todos.value.filter((t) => !t.done) : todos.value
);

watch(todoId, fetchData);
React
const filteredTodos = () => {
  return hideCompleted ? todos.filter(todo => !todo.done) : todos;
}

useEffect(() => {
  fetchTodo();
}, [todoId]);

テンプレート内の記述

  • Vueではv-ifv-forと独特の構文を使用します
  • ReactではJSX構文を使用します
Vue
<template>
~省略~
  <input :value="text" @input="onInput" placeholder="Type here" />
~省略~
  <button @click="toggle">toggle</button>
  <h1 v-if="awesome">Vue is awesome!</h1>
  <h1 v-else>Oh no 😢</h1>
~省略~
  <form @submit.prevent="addTodo">
    <input v-model="newTodo" />
    <button>Add Todo</button>
  </form>
  <ul>
    <li v-for="todo in filteredTodos" :key="todo.id">
      <input type="checkbox" v-model="todo.done" />
      <span :class="{ done: todo.done }">{{ todo.text }}</span>
      <button @click="removeTodo(todo)">X</button>
    </li>
  </ul>
~省略~
</template>
React
return (
    <div>
~省略~
        <input type={"text"} placeholder={'Type here'} onChange={handleChangeText}/>
~省略~
        <button onClick={handleClickToggle}>toggle</button>
        {awesome ? (<h1>React is awesome!</h1>) : <h1>Oh no 😢</h1>}
~省略~
        <input type={"text"} value={newTodo} onChange={(e) => setNewTodo(e.target.value)} />
        <button onClick={handleOnClickAddTodo}>Add Todo</button>
        <ul>
            {
                filteredTodos().map((todo) => {
                    return (
                        <li key={todo.id}>
                            <input type="checkbox" checked={todo.done} onChange={() => handleOnChangeCheckTodo(todo.id)} />
                            <span>{todo.text}</span>
                            <button onClick={() => handleOnChangeDeleteTodo(todo.id)}>X</button>
                        </li>
                    );
                })
            }
        </ul>
~省略~
    </div>
);

比較してみて

  • Vueを書いているときは、Reactよりも書きやすくコード量も少なく書けている印象でしたが、結果にそこまで差はありませんでした
  • 自分はReactを本格的に触ってまだ1年程度ですが、Vueの方が書きやすいと感じました
    • 人生で一番最初に触れたのがHTML + smartyで、ReactよりはVueの書き方が近いように感じたので、同様の方ならVueの方がとっつきやすい印象になるかもしれません
  • 逆にReactから入った方だと 、Vueの構文を新たに覚えるのが大変かもしれないです

VueでTypeScriptも触ってみた

  • Reactと比べて、VueはTypeScriptのサポートがまだイマイチということだったので、試しにTypeScriptでも書いてみました
/Sample.vue
<script setup lang="ts">
import { computed, ref, onMounted, watch } from 'vue'

interface Todo {
  id: number
  text: string
  done: boolean
}

const text = ref<string>('')
const awesome = ref<boolean>(true)
let id = 0
const newTodo = ref<string>('')
const todos = ref<Todo[]>([
  { id: id++, text: 'Learn HTML', done: true },
  { id: id++, text: 'Learn JavaScript', done: true },
  { id: id++, text: 'Learn Vue', done: false }
])
const hideCompleted = ref<boolean>(false)
const filteredTodos = computed(() =>
  hideCompleted.value ? todos.value.filter((t) => !t.done) : todos.value
)
const pElementRef = ref<HTMLElement | null>(null)
const todoId = ref<number>(1)
const todoData = ref<any>(null)

const onInput = (e: Event) => {
  text.value = (e.target as HTMLInputElement).value
}

const toggle = () => {
  awesome.value = !awesome.value
}

const addTodo = () => {
  todos.value.push({ id: id++, text: newTodo.value, done: false })
  newTodo.value = ''
}

const removeTodo = (todo: Todo) => {
  todos.value = todos.value.filter((t) => t !== todo)
}

onMounted(() => {
  if (pElementRef.value) {
    pElementRef.value.textContent = 'mounted!'
  }
})

const fetchData = async () => {
  todoData.value = null
  const res = await fetch(`https://jsonplaceholder.typicode.com/todos/${todoId.value}`)
  todoData.value = await res.json()
}

fetchData()

watch(todoId, fetchData)
</script>

<template>
  <h1>Composition Api Sample</h1>
  <input :value="text" @input="onInput" placeholder="Type here" />
  <p>ここに表示:{{ text }}</p>
  <br />
  <button @click="toggle">toggle</button>
  <h1 v-if="awesome">Vue is awesome!</h1>
  <h1 v-else>Oh no 😢</h1>
  <br />
  <form @submit.prevent="addTodo">
    <input v-model="newTodo" />
    <button>Add Todo</button>
  </form>
  <ul>
    <li v-for="todo in filteredTodos" :key="todo.id">
      <input type="checkbox" v-model="todo.done" />
      <span :class="{ done: todo.done }">{{ todo.text }}</span>
      <button @click="removeTodo(todo)">X</button>
    </li>
  </ul>
  <button @click="hideCompleted = !hideCompleted">
    {{ hideCompleted ? 'Show all' : 'Hide completed' }}
  </button>
  <br />
  <p ref="pElementRef">hello</p>
  <br />
  <p>Todo id: {{ todoId }}</p>
  <button @click="todoId++">Fetch next todo</button>
  <p v-if="!todoData">Loading...</p>
  <pre v-else>{{ todoData }}</pre>
</template>
  • ざっと書いてみた感じでは、ReactでTypeScriptを書くのとあまり変わらない印象でした

感想

  • かなり簡単なコードでの比較となるのであまりあてにはならないかもしれませんが、VueとReactで書き方に大きな違いはないと感じました
  • ここにcssなどのデザイン周りのコードや特殊な機能を持つ画面を追加していくと、また違った印象になるかもしれません
  • 個人的にはもっとVueに触れてみたいなという気持ちが湧きました

参考

Vue

Vue(TypeScript)

React

EGSTOCK,Inc.

Discussion

Honey32Honey32

長文失礼します。 React の部分について、参考になれば。

React に込められた設計の意図を理解するとかなり使いやすくなるので、良かったら公式 Docs を読んでみてください。(まだ Google 検索に公開されたばかりなので、検索に載っていませんが良記事ばかりです。)

https://ja.react.dev/learn

Vue の computed に対応するのは「再レンダリングのたびに全て再計算される」デフォルト挙動

-    const filteredTodos = () => {
-        return hideCompleted ? todos.filter(todo => !todo.done) : todos;
-    }
+   const filteredTodos = hideCompleted ? todos.filter(todo => !todo.done) : todos;

React のコンポーネントの内側に書いた式は、再レンダリングのたびにすべて再計算されます。(map 関数の中に渡した関数のようなものです。)

https://qiita.com/honey32/items/58e56e407d4d87e294a4

なので、Vue において computed を使っていた箇所は、全て「ただの式」に置き換え可能です。

(一応、再計算が重いときのために useMemo もあります。)

コンポーネントの中に初期値を書かない

(再びですが) React のコンポーネントの内側に書いた式は、再レンダリングのたびにすべて再計算されます。なので、初期値を構築する式は、コンポーネント内に直に書かないほうが分かりやすくなります。

コンポーネントの外側か、useState の引数に入れることができます。

https://zenn.dev/yumemi_inc/articles/react-lifetime-of-variable#4.-ファイル-――-コンポーネントよりも前から存在し、最も長生きする

const createDefaultTodos = () => {
  let id = 0;
  return [
    {id: id++, text: 'Learn HTML', done: true,},
    {id: id++, text: 'Learn JavaScript', done: true,},
    {id: id++, text: 'Learn React', done: false,}
  ];
}

const defaultTodos = createDefaultTodos();

// 省略

export default function Index() {
  const [todos, setTodos] = useState(defaultTodos)
  // または、defaultTodos に一度保存せずに useState(createDefaultTodos) と書いても OK です。

useState の引数に、初期値を生成する関数 createDefaultTodos を渡すことも可能です。(初期化関数 / initializer function)

https://ja.react.dev/reference/react/useState#avoiding-recreating-the-initial-state

依存配列で嘘をつかない

依存配列についてですが、これは「fetchTodo は todoId が変わるたびに実行したいので、todoId を依存配列に入れよう」と考えるのではなく、「useEffect の中の関数で使われている変数をすべて列挙する」ようなものです。(しかも、何も考えずに eslint exhaustive-deps ルールに任せさえすれば、勝手に「更新されなくて困る」ことが無くなります)

https://zenn.dev/yumemi_inc/articles/react-effect-simply-explained#3.-やっと登場する依存配列、eslint-に列挙してもらおう

-  useEffect(() => {
-      fetchTodo();
-  }, [todoId]);
+  useEffect(() => {
+    const fetchTodo = async () => {
+      setTodoData(null);
+      try {
+        const res = await fetch(`https://jsonplaceholder.typicode.com/todos/${todoId}`);
+        const data = await res.json();
+        setTodoData(data);
+      } catch (error) {
+        console.error(`error: ${error}`);
+      }
+    }
+    fetchTodo();
+  }, [todoId]);
サブサブ

ありがとうございます!
参考にさせていただきます 🙏