ReactとVueの比較、全く同じアプリを作成してみて分かった相違点 2019年Edition
Post on:2019年10月17日
日常的にVueを使用している開発者が、ReactとVueで全く同じアプリを作成した時のそれぞれの工程を比較して分かった相違点を紹介します。
特に、Reactのフックについて具体的な使い方が解説されています。
I created the exact same app in React and Vue. Here are the differences. [2019 Edition]
by Sunil Sandhu
下記は各ポイントを意訳したものです。
※当ブログでの翻訳記事は、元サイト様にライセンスを得て翻訳しています。
本記事は以前翻訳した記事の2019年Editionで、Reactのフックが追加されています。以前の記事は下記をご覧ください。
- 隣の家の芝生は青く見える
- ReactとVueで作成したアプリの見た目を比較
- ReactとVueはデータをどのように変更させるか
- アイテムの新規作成
- アイテムの削除
- イベントリスナーを渡す
- 子コンポーネントにデータを渡す
- 親コンポーネントにデータを戻す
- 終わりに
隣の家の芝生は青く見える
私は現在の職場でVueを使用しており、Vueがどのように機能するかかなり理解していると思います。しかし、隣の家の芝生はどんなものなのか非常に興味があります、その芝生とはReactのことです。
Reactのドキュメントを一通り読み、チュートリアルの動画をいくつか見てみました。それは参考になったのですが、私が本当に知りたかったのは、ReactとVueがどのように異なるのかということでした。異なるというのは、仮想DOMとかレンダリングなどではありません。コードレベルで具体的にどう違うのかが知りたいのです。
しかし、いくら探しても見つけることができませんでした。そこで私は、類似点と相違点を知るためには、自分自身で記事を書かなければならないことを認識しました。この記事がここに存在するように、私はプロセス全体をドキュメント化しました。
VueとReact
ドキュメントを書くにあたり、TODOリストのアプリを作成しました。機能は、アイテムを加えたり、削除できるだけの簡単なアプリです。どちらもデフォルトのCLI(Reactではcreate-react-app、Vueではvue-cli)を使用しました。
※CLIとは、Command Line Interfaceの略。
ReactとVueで作成したアプリの見た目を比較
左: Reactで作成したアプリ、右: Vueで作成したアプリ
CSSのコードは両方とも全く同じですが、置き場所が異なります。そのことを念頭において、アプリのファイル構造を見てみましょう。
左: Reactのファイル構造、右: Vueのファイル構造
VueとReactで、ファイル構造はほとんど同じです。唯一違うのは、Reactには3つのCSSファイルがあることです。VueにはCSSファイルが一つもありません。その理由は、create-react-appではReactコンポーネントがスタイルを保持するためのファイルを持ち、Vue CLIでは実際のコンポーネントファイル内でスタイルが宣言されているすべての包括的なアプローチを採用しているからです。
追記: 今になって考えると、Reactは3つのCSSファイルで構成されている方が理にかなっているので、App.js, ToDo.js, ToDoItem.js用に個別に分けてCSSファイルを用意します。
最終的にはどちらも同じことを実現しますが、ReactとVueでCSSの構造を変えることはできません。これは完全に個人の好みによります。CSSをどのように構造化するべきか、特にReactに関してはCSS-in-JSのソリューションがあるので、開発者コミュニティで活発に議論されています。CSS-in-JSは文字通りで便利ですが、現時点では両方のCLIに配置された構造に従います。
先に進む前に、ReactとVueの典型的なコンポーネントがどのように見えるか簡単に見てましょう。
Reactのコンポーネント
Vueのコンポーネント
では、細かい点について詳しく説明します。
ReactとVueはデータをどのように変更させるか
まずは「データの変更」とは、どういう意味でしょうか? 変更とは、保存したデータを変えることです。例えば、名前の値をJohnからMarkに変更するのであれば、「データの変更」になります。ここにReactとVueに大きな違いがあります。Vueは基本的にデータを自由に更新できるデータオブジェクトを作成しますが、Reactはステートフックと呼ばれる方法で処理します。
下記の画像で、ReactはステートフックとVueのデータオブジェクトを見てみましょう。その後、何が起こっているのかを説明します。
左: Reactのステートフック、右: Vueのデータオブジェクト
両方とも同じデータを渡していますが、構造は少し異なります。
Vueでは、コンポーネントのすべての可変データをdata()関数内に配置し、この関数がデータを含むオブジェクトを返します(画像の右)。
Reactでは(少なくとも2019年以降)通常、フックを通じてステートを処理します。こういったコンセプトを見たことがない人には、最初は少し奇妙に見えるかもしれません。基本的には次のように動作します。
例えば、TODOリストを作成するとします。listという変数を作成する必要があり、この変数はストリングまたはオブジェクトのいずれかの配列を取る可能性があります (例えば、各todoストリングにIDとその他のものを与えたい場合など)。これを設定するには、const [list, setList] = useState([])と記述します。ここではReactがフックと呼んでいるuseStateを使用します。これにより、コンポーネント内でローカル状態を維持できます。
また、useState()内に空の配列[]を渡したことに気がついたかもしれません。その中には、最初にリストを設定したいものが入っています。ここでは、空の配列にします。ただし、上の画像を見ると分かるように、配列の内部にいくつかのデータを渡しており、これがlistの初期化データになります。setListは何をするのでしょうか。これについては、後述します。
アプリ内の可変データをどのように参照すればよいか
例えば、「Sunil」という値が割り当てられたnameというデータがあるとします。
Vueでは、これはdata()オブジェクト内にあり、name: ‘Sunil'と呼ばれます。アプリでは、this.nameで参照できます。同様に、this.name = ‘John’と呼び出すと、変更することもできます。これで名前はJohnに変更されます。Johnと呼ばれてどう思うか分かりませんが、いろいろなことが起こりますよ😅
Reactでは、useState()で作成したコードがあるため、const [name, setName] = useState('Sunil')を使用します。アプリではnameで同じデータを参照します。ここでの重要な相違点は、単にname='John'と記述することができないということです。Reactでは簡単に変更できないようにする制限があるからです。したがって、ReactではsetName('John')と記述します。ここではsetNameが役立ちます。基本的に、const [name, setName] = useState('Sunil')では、2つの変数を作成します。1つ目はconst name = 'Sunil'で、2つ目はconst setNameには新しい値でnameを再作成できる関数が割り当てられます。
実質的にはReactもVueも同じことをしており、変更可能なデータを作成します。Vueはデータの一部が更新される度にデフォルトでnameとsetNameを組み合わせます。つまり、Reactではstateを更新するためにsetName()内で値を呼び出す必要がありますが、Vueではデータオブジェクト内の値を更新しようとした場合に処理を行うと想定しています。
では、Reactは関数から値を分離するのに苦労しているのなぜでしょうか。また、useState()が必要なのはなぜでしょうか。この説明はRevanth Kumarに委ねます。
それは、Reactがstateが変わるたびに特定のフックを再実行しようとしているためです。例えば、componentWillReceiveProps, shouldComponentUpdate, componentWillUpdate, render, componentDidUpdate, whenever state changes など。setState関数を呼び出すと、stateが変更されたことが分かるでしょう。もし、stateを直接的に変更させた場合、Reactは変更を追跡し、フックを実行するなど、もっと多くの作業を行う必要があります。それを簡単にするために、ReactはsetStateを使用します。
一方はシンプルじゃないね
データの変更について、ReactとVueの相違を理解できたので、アプリ作成に戻ります。
アイテムの新規作成
Reactの場合
1 2 3 4 5 6 |
const createNewToDoItem = () => { const newId = Math.max.apply(null, list.map((t) => t.id)) + 1 const newToDo = { id: newId, text: toDo }; setList([...list, newToDo]); setToDo(""); }; |
Reactの場合、inputフィールドにvalueという属性があります。このvalueは、onChangeイベントリスナーによって値が変更されるたびに自動的に更新されます。JSX(これは基本的にはHTMLの一種)は、下記のようになります。
1 2 3 |
<input type="text" value={toDo} onChange={handleInput}/> |
これにより、valueが変更される度に状態が更新されます。handleInput関数は下記のようになります。
1 2 3 |
const handleInput = (e) => { setToDo(e.target.value); }; |
これでユーザーがページ上の+ボタンを押して新しいアイテムを追加する度に、createNewToDoItem関数がトリガーされます。このfunctionをもう一度見て、何が起こっているのかを確認してみましょう。
1 2 3 4 5 6 7 |
const createNewToDoItem = () => { const newId = Math.max.apply(null, list.map((t) => t.id)) + 1 const newToDo = { id: newId, text: toDo }; setList([...list, newToDo]); setToDo(""); }; |
基本的に、newId関数は新しいToDoアイテムに与える新しいIDを作成します。newToDo変数はnewIdから値を受け取るidキーを持つオブジェクトです。また、toDoの値を値として受け取るtextキーもあります。これは入力値が変更される度に更新されていたtoDoと同じです。
次にsetList関数を実行し、list全体と新しく作成されたnewToDoを含む配列を渡します。
...listの記述は、先頭の3つのドットはスプレッド演算子で、単純に配列全体を渡すのではなく、基本的にlistのすべての値を別々の項目として渡すものです。もし混乱したなら、スプレッドについて勉強することを強くお勧めします。
最後にsetToDo()を実行して、空の文字列を渡します。これで入力値が空になり、新しいtoDoを入力できるようになります。
Vueの場合
1 2 3 4 5 6 |
createNewToDoItem() { const newId = Math.max.apply(null, this.list.map(t => t.id)) + 1; this.list.push({ id: newId, text: this.todo }); this.todo = ""; } |
Vueの場合、inputフィールドにv-modelと呼ばれるハンドルを使用します。このハンドルにより、双方向バインディングを行うことができます。inputフィールドのコードを見てましょう。
1 |
<input type="text" v-model="todo"/> |
v-modelは、このフィールドに入力された値をtoDoItemというデータオブジェクト内にあるキーに関連付けます。ページが読み込まれると、toDoItemには「todo: ''」のような空の文字列が設定され、「todo: 'add some text here'」のようなデータが既にある場合は、inputフィールドにテキストを追加して読み込みます。いずれにしても、空の文字列として使用すると、inputフィールド内に入力したテキストはtodoの値にバインドされます。これは実質的に、双方向バインディングと同じです(inputフィールドはデータオブジェクトを更新でき、データオブジェクトはinputフィールドを更新できます)。
先ほどのcreateNewToDoItem()を振り返ってみると、todoの内容をlistの配列にプッシュしてから、todoを空の文字列に更新することが分かります。
また、Reactの例で使用したのと同じnewId()関数も使用しました。
アイテムの削除
Reactの場合
1 2 3 |
const deleteItem = (item) => { setList(list.filter((todo) => todo !== item)); }; |
deleteItem()関数はToDo.js内にありますが、<ToDoItem/>のpropとしてdeleteItem()関数を渡すことでToDoItem.jsで簡単に参照できます。
1 |
<ToDoItem deleteItem={deleteItem}/> |
最初に、このコードで子にアクセスするために関数を渡します。次に、ToDoItemコンポーネント内で下記を記述します。
1 |
<button className="ToDoItem-Delete" onClick={() => deleteItem(item)}> - </button> |
親コンポーネント内にある関数を参照するために必要なのは、this.props.deleteItemを参照するだけです。コードでは、props.deleteItemの代わりにdeleteItemにしていることに気づいたかもしれません。これはpropsオブジェクトの一部を取得して変数に割り当てることができるデストラクチャリングと呼ばれる手法を使用したためです。そのため、ToDoItem.js内にも下記のコードが記述してあります。
1 2 3 |
const ToDoItem = (props) => { const { item, deleteItem } = props; } |
このコードによって2つの変数が作成され、1つはprops.itemと同じ値が割り当てられるitemで、もう1つはprops.deleteItemから値が割り当てられるdeleteItemです。このprops.itemとprops.deleteItemを使用するだけでデストラクチャリングを回避でき、ここで言及する価値があると思います。
Vueの場合
1 2 3 |
onDeleteItem(item){ this.list = this.list.filter(todo => todo !== item); } |
Vueの場合、少し異なるアプローチが必要です。ポイントは3つです。
最初に、関数を呼び出す要素を用意します。
1 |
<div class=”ToDoItem-Delete” @click=”deleteItem(item)”>-</div> |
次に、子コンポーネント(この場合はToDoItem.vue)内のメソッドとしてemit関数を作成する必要があります。これは下記のようになります。
1 2 3 |
deleteItem(item) { this.$emit('delete', item) } |
これに加えて、ToDo.vue内にToDoItem.vueを追加すると、実際に関数を参照することに気づくでしょう
1 2 3 4 |
<ToDoItem v-for="todo in list" :todo="todo" @delete="onDeleteItem" // <-- this :) :key="todo.id" /> |
これは、カスタムイベントリスナーと呼ばれるものです「delete」の文字列でemitがトリガーされるあらゆる状況で実行されます。イベントが起きると、onDeleteItemという関数が呼び出されます。この関数は、ToDoItem.vueではなくToDo.vue内にあります。この関数は前述のように、dataオブジェクト内のtodo配列をフィルタリングして、クリックされたアイテムを削除します。
またVueの場合、@clickに$emit()を書くだけでも大丈夫です。
1 |
<div class=”ToDoItem-Delete” @click=”$emit(‘delete’, item)”>-</div> |
この記述で工程を減らすことができますが、どちらを採用するかは個人の好みです。
まとめると、Reactでは子コンポーネントはthis.propsを介して親の関数にアクセスします(かなり一般的な方法)。Vueでは親コンポーネント内で通常収集されるイベントを子から送出する必要があります。
イベントリスナーを渡す
Reactの場合
クリックイベントなどの単純なイベントリスナーは簡単です。ここでは、新しいToDoアイテムを作成するボタンのクリックイベントを例にします。
1 |
<button className=”ToDo-Add” onClick={createNewToDoItem}>+</div>. |
コードは非常に簡単で、onClickにインラインのJavaScriptで処理しているかのように見えます。Vueで説明したように、enterキーが押されるたびにイベントリスナーを設定するのには少し時間がかかりました。下記のように、input要素に記述されたonKeyPressイベントで処理します。
1 |
<input type=”text” onKeyPress={handleKeyPress}/>. |
この関数はenterキーが押されたことを認識する度に、createNewToDoItem関数を呼び出します。
1 2 3 4 5 |
handleKeyPress = (e) => { if (e.key === ‘Enter’) { createNewToDoItem(); } }; |
Vueの場合
Vueでは、非常に簡単です。@を使用して、実行したいイベントリスナーのタイプを記述するだけです。例えば、クリックイベントを加えるには、下記のようにします。
1 |
<button class=”ToDo-Add” @click=”createNewToDoItem()”>+</div> |
注: @clickは、v-on:clickの省略形です。
Vueのイベントリスナーの良い点は、イベントリスナーが複数回トリガーされないようにする.onceなど、連鎖できるものがたくさんあることです。また、キーストロークを処理するために特定のイベントリスナーを作成する場合には、いくつかのショートカットがあります。
enterキーが押された度に、Reactでイベントリスナーを作成して新しいToDoアイテムを作成するのにはかなり時間がかかりました。Vueでは、非常に簡単に書くことができます。
1 |
<input type=”text” v-on:keyup.enter=”createNewToDoItem”/> |
子コンポーネントにデータを渡す
Reactの場合
Reactでは、作成されたポイントで子コンポーネントにpropsを渡します。
1 |
<ToDoItem key={key.id} item={todo} /> |
このコードでは、ToDoItemコンポーネントに2つのpropsが渡されているのを確認できます。これにより、this.propsを介して子コンポーネントでそれらを参照することができます。item.todo propにアクセスするには、単にthis.props.itemを呼び出すだけです。
Vueの場合
Vueでは、子コンポーネントが作成された時点でpropsを渡します。
1 2 3 4 |
<ToDoItem v-for="item in list" :item="item" @delete="onDeleteItem" :key="item.id" /> |
子コンポーネントにprops: ['todo']のように配列でpropsを渡しています。これらの名前は子で参照することができ、この場合は「'todo'」です。
親コンポーネントにデータを戻す
Reactの場合
Reactでは最初に、子コンポーネントと呼ばれる場所のpropとして参照することによって、子コンポーネントに関数を渡します。次に、this.props.whateverTheFunctionIsCalledを参照して、onClickなどを使用して、関数の呼び出しを子関数に加えます。これにより、親コンポーネントにある関数がトリガーされます。
このプロセスの例は、「アイテムの削除」をご覧ください。
Vueの場合
Vueでは子コンポーネントで、親の関数に値を返す関数を書くだけです。親コンポーネントでは、その値がいつ呼び出されるかを監視する関数を作成します。この関数は、関数の呼び出しをトリガーにできます。
このプロセスの例は、「アイテムの削除」をご覧ください。
終わりに
データの追加、削除、変更、親から子へデータを渡し、子から親にデータを渡す方法を解説しました。もちろん、ReactとVueでたくさんの相違点がありますが、2つのフレームワークがどのように扱うのかを理解するための助けになると思います。
最後に、ReactとVueで作成したアプリのコードを紹介します。
sponsors