2019.09.17

Vue.jsエンジニアのためのReact入門


この記事では、Vue.js は覚えたけど React もやってみたい、というエンジニア向けに、Vue.js における各機能の React での実現方法を比較して紹介します。

私は、慣れない言語やフレームワークを使うときはマニュアルを頭から読むより、「あの機能はこっちではどうやるのかな?」と考えて調べることが多いので、自分自身の React の学習も兼ねてまとめました。

コンポーネントの機能

コンポーネントの作り方

Vue.js

Vue コンポーネントは ComponentName.vue のように、.vue 拡張子を持つファイルで表現されますね。<template> <script> <style> それぞれのブロックにテンプレートや振る舞い、スタイル定義を記述します。

<template>
  <div>
    <h1>Sample</h1>
    <h2>This is a component.</h2>
  </div>
</template>

<script>
export default {
  // dataやmethodsなど...
}
</script>

<style>
/* スタイル定義 */
</style>

React

React には、コンポーネントを作る方法が、大きく分けて二通りあります。関数型とクラス型です。

まずは関数型コンポーネントです。

import React from 'react';

function Sample() {
  return (
    <div>
      <h1>Sample</h1>
      <h2>This is a component.</h2>
    </div>
  );
}

export default Sample;

関数型コンポーネントは React 要素を返却する関数です。一見、JavaScript の関数が HTML を返却しているように見えますが、これは HTML とは似て非なる JSX という拡張構文です。

JSX の記述される React 要素が、画面にレンダリングされるべき UI を定義します。Vue.js で言うと、<template> に当たるでしょう。

また、見た目上は使用されていませんが、かならず冒頭で React をインポートする必要があります。

次に以下がクラス型コンポーネントです。

import React from 'react';

class Sample extends React.Component {
  render() {
    return (
      <div>
        <h1>Sample</h1>
        <h2>This is a component.</h2>
      </div>
    );
  }
}

export default Sample;

クラス型コンポーネントでは、render メソッドが React 要素を返却します。

二つの方法があると、どちらを使えばいいのか迷いますね。以前のバージョンまでは、クラス型には状態管理やライフサイクルといった追加機能があるとされていました。
(ライフサイクルの概念は Vue.js にもありますね。createdmounted です。)

しかしバージョン 16.8 で関数型コンポーネントのためのフックという機能が追加され、関数型コンポーネントでも状態管理やライフサイクルの機能にアクセスできるようになりました。

まだ(v16.9.0時点)フックは、クラス型の持つ機能を完全に再現できていません。しかし React 開発チームの展望としては、フックへの機能追加を積極的に行い、関数型をメインの方法にしてもらいたいと考えているようです。

長期的には、フックが React のコンポーネントを書く際の第一選択となることを期待しています。

フック、クラスのいずれを使うべきですか、あるいはその両方でしょうか?

これから学習や開発を始める場合は、まず関数型で書いて、必要な場合のみクラス型にする、という方針になるかと思います。いまのところクラス型が必要になるのは、フックでサポートされていないライフサイクルにアクセスしたい場合でしょう。

本記事では、関数型コンポーネントをメインに説明していきます。

テンプレート

Vue.js

Vue.js の <template> には、v-bindv-forv-if などのテンプレート構文が用意されています。

<template>
  <div>
    <h1>Hello, {{ name }} !</h1>
    <a :href="link">Click me</a>
    <ul>
      <li v-for="item in items" :key="item.key">
        {{ item.title }}
      </li>
    </ul>
    <p v-if="isActive">Paragraph</p>
    <p v-show="isShow">Second paragraph</p>
  </div>
</template>

React

React の場合は、JSX で表現していきます。

return (
  <div>
    <h1>Hello, {name} !</h1>
    <a href={link}>Click me</a>
    <ul>
      {items.map(item => (
        <li key={item.key}>{item.title}</li>
      ))}
    </ul>
    {isActive && <p>Paragraph</p>}
    <p style={{ display: isShow ? 'initial' : 'none' }}>Second paragraph</p>
  </div>
);
  • JSX 内で変数を展開する場合は、中括弧 {} で囲みます。
  • JSX は厳密にはテンプレートエンジンではなく、あくまで JavaScript の拡張なので、iffor などの独自の制御構文は存在しません。JavaScript の map メソッドや論理演算子で制御構造を表現します。
  • v-show にあたる構文も存在しないので、同様の実装を得たい場合は直接 style を操作することになるでしょう。
  • CSS クラスについては後述します。

Vue.js の <template> と同様に、React でもコンポーネントを記述する際にはルート要素を一つだけ返却しなければいけません。しかし、React.Fragment を使用することで複数のルート要素を返却することができます。フレームワークのためだけに本来不要な <div> で囲む、なんてことをしなくてよくなるわけです。

return (
  <React.Fragment>
    <h1>...</h1>
    <h2>...</h2>
    <h3>...</h3>
  </React.Fragment>
);

上記は以下のように書き換えることができます。

return (
  <>
    <h1>...</h1>
    <h2>...</h2>
    <h3>...</h3>
  </>
);

なんとも不思議な構文ですね!

CSS クラス

Vue.js

Vue.js では、クラス属性は v-bind:class で記述します。

<!-- オブジェクト -->
<button class="btn" :class="{ 'btn-primary': isPrimary }">...</button>

<!-- 配列 -->
<button class="btn" :class="['btn-primary', 'btn-small']">...</button>

React

React の場合は、className 属性を用います。

return <button className="btn btn-primary">...</button>;

className 属性は、Vue.js の v-bind:class と比べると単純で、動的な配列やオブジェクトを指定することはできません。条件に応じてクラスを使い分けたいときは、自分で className の属性値となる文字列を操作する処理を書く必要があります。

ただそのようなユースケースはありふれているため、className の扱いをより便利にする、classnames というデファクトスタンダードなライブラリが存在します。

import classNames from 'classnames'

Vue.js とほぼ同じように、配列やオブジェクトで動的な指定が可能になります。

const buttonClass = classNames({
  btn: true,
  'btn-primary': isPrimary
});

return <button className={buttonClass}>...</button>;
const buttonClass = classNames('btn', 'btn-primary', 'btn-small');

return <button className={buttonClass}>...</button>;

また React には、Vue.js が持つ「クラスのマージ」機能は存在しません。Vue.js では classv-bind:class(または省略形である :class)を併記すると、最終的に二つの属性の値がマージされてレンダリングされます。しかし React の場合は、一つの要素に className を二つ以上記述しても、コンパイラが警告を吐いて、後に書かれた className の指定が勝つだけです。

HTML

Vue.js

Vue.js コンポーネントに HTML 文字列を挿入するときは、v-html 属性を用います。

<div v-html="htmlString"></div>

React

React では dangerouslySetInnerHTML という長い名前の属性に、__html というキーを持つオブジェクトを指定します。

return <div dangerouslySetInnerHTML={{ __html: htmlString }} />;

イベント

Vue.js

Vue.js におけるイベント処理は、v-on またはその省略形である @ 構文によって実現されます。

<button @click="handleClick">Click me</button>

<!-- イベント修飾子 -->
<form @submit.prevent="handleSubmit">...</form>

React

React では、より HTML に似た構文が用いられます。

<button onClick={handleClick}>Click me</button>

キャメルケースでイベント名を指定します。

Vue.js での .prevent のようなイベント修飾子は存在しません。

ステート管理

Vue.js

Vue.js では、data メソッドの返却値によって、コンポーネントの内部状態(ステート)を定義します。

<script>
export default {
  data() {
    return {
      count: 0
    }
  }
}
</script>

React

React では、useState フックを用います。

import React, { useState } from 'react'

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

  const handleClick = () => setCount(count + 1);

  return <button onClick="handleClick">{count}</button>;
}

すべてのフックは、React の機能を利用するための関数です。useState フックは、ステート管理の機能を利用するための関数です。

useState は、ステートの初期値を引数に取り、「ステート変数」と「ステート変数を更新するための関数」を配列で返却します。ステート変数は、useState から返された関数を通してしか更新することができません。

管理したいステートが複数ある場合は、useState を複数実行します。

const [name, setName] = useState("John Doe");
const [age, setAge] = useState(20);

または、オブジェクトを状態に持つステートを定義することもできます。

const [user, setUser] = useState({ name: "John Doe", age: 20 });

オブジェクトを指定できるとはいえ、何でもかんでも一つのステート(一度の useState コール)に詰め込むのではなく、少なくとも意味のある塊に分けて管理するのが良さそうです。

フォーム

Vue.js

Vue.js では v-model でフォームの入力値を扱います。

<input type="text" v-model="name" />

v-model を使うことによってデータの流れが双方向になります。つまり、画面上でフォームの入力値を変更するとコンポーネント内のデータ(上の例で言うと this.name)も自動的に更新されます。

React

React の場合は、自動的な双方向のデータ更新を実装していません。「フォームの入力値を変更するとコンポーネント内のステートも更新される」という処理は自作する必要があります。

const [name, setName] = useState('');
const handleInput = e => setName(e.target.value);

return <input type="text" onChange="handleInput" value="name" />;

この処理は結局フォームを扱う際には常に必要になるので、都度定義しないといけないのは Vue.js と比べると不便だと感じますが、親切(お節介?)な機能は極力削るストイックな姿勢が React らしさなのかもしれません。

コンポーネントメソッド

Vue.js

Vue.js では methods にメソッドを定義します。ここで定義されたメソッドはテンプレートからアクセスできます。

<script>
export default {
  methods: {
    sayHello() {
      console.log(`Hello, ${this.name}!`)
    }
  }
}
</script>

React

React には同様の機能は特にありません。Vue.js のような <templte><script> の区分がそもそもないので、必要ないのでしょう。JSX を return するスコープから参照できる場所に、普通に関数を定義すれば OK です。

function MyComponent() {
  const [name, setName] = useState('');

  function sayHello() {
    console.log(`Hello, ${name}!`);
  }

  return <button onClick={sayHello}>...</button>;
}

ref

Vue.js

Vue.js では ref 属性で名前をつけると、DOM 要素に直接アクセスできます。

<template>
  <div>
    <div ref="target">...</div>
    <button @click="handleClick">Click me</button>
  </div>
</template>

<script>
export default {
  methods: {
    handleClick() {
      console.log(this.$refs.target);
    }
  }
}
</script>

Vue や React を前提としていない他の UI ライブラリを組み合わせたい場合などに使用する機能ですね。

React

React にも同様の機能は備わっていて、属性名も Vue と同じ ref です。Vue のように名前をつけるというより、ref 属性に参照用の変数を与えます。

useRef フックで DOM 参照用の変数を作成します。DOM 要素には、current プロパティからアクセスすることができます。

import React, { useRef } from 'react';

function MyComponent() {
  const target = useRef(null);

  const handleClick = () => {
    console.log(target.current);
  };

  return (
    <>
      <div ref={target}>...</div>
      <button onClick={handleClick}>Click me</button>
    </>
  );
}

export default MyComponent;

算出プロパティ

Vue.js

Vue.js には、算出プロパティという機能が備わっています。

<p>Hello, {{ fullName }} !</p>
export default {
  data() {
    return {
      firstName: 'John',
      lastName: 'Doe'
    };
  },
  computed: {
    fullName() {
      return `${this.firstName} ${this.lastName}`;
    }
  }
}

ステートを元にした計算ロジックをまとめられる、計算結果をキャッシュできる(計算に使用されるステートに変更があったときのみ再計算される)というメリットがあります。

React

React には、明示的には算出プロパティという機能はありません。useMemo フックを利用すると、似た機能を得ることができるようです。

import React, { useMemo } from 'react';

function MyComponent() {
  const [firstName, setFirstName] = useState("John");
  const [lastName, setlastName] = useState("Doe");

  const getFullName = () => `${firstName} ${lastName}`;
  const fullName = useMemo(getFullName, [firstName, lastName]);

  return <p>Hello, {fullName} !</p>;
}

export default MyComponent;

useMemo は、第一引数に関数を、第二引数に配列を取り、「メモ化」された値を返します。「メモ化」というのは、関数の結果をキャッシュして次回呼び出し時の再計算を防ぐプログラミングテクニックです。

つまり、コンポーネントがレンダリングされる際、第二引数に配列で渡された変数の値に変更があったときのみ、第一引数に渡した関数が再実行され、返却値が更新されます。第二引数の値に変更がなかった場合はキャッシュされた結果が返されます。

この動作は、Vue.js の算出プロパティと似ていますね。ただ、Vue.js における算出プロパティほど多用されるパターンでもなさそうです。useMemo はあくまでパフォーマンス上意味がある場合のみ使用します。

たとえば、上記の単純な例では再計算されたところでパフォーマンスには影響はなく、逆に useMemo 自体の実行コストがかかるので、あまり利用する意味がない、ということになります。

watch

Vue.js

算出プロパティに加え、Vue.js には監視プロパティ watch も用意されています。

export default {
  watch: {
    name(newVal, oldVal) {
      // newVal -> 変更後の値
      // oldVal -> 変更前の値
    }
  }
}

あるステートの変更されたタイミングで処理を行う機能です。

React

React には監視プロパティのような機能は存在しません。

しかし、Vue.js でも watch のユースケースは少ないので、困ることはないでしょう。

filter

Vue.js

Vue.js ではテンプレートでフィルターを使用できます。

<p>¥{{ price | formatNumber }}</p>

出力値を加工する仕組みですね。

React

React にはフィルターは存在しません。

Vue.js におけるフィルターは結局は単なる関数をテンプレート内でクリーンに見せているだけなので、React でも普通に関数を用いればよいでしょう。

const formatNumber = num => {
  // ...
};

return <p>{formatNumber(price)}</p>

ライフサイクル

ここからはライフサイクルについて紹介します。Vue.js 同様、React にもコンポーネントのライフサイクルが存在します。ただ、関数型コンポーネントのライフサイクルにはフックを用いてアクセスするため、記述が独特です。よりわかりやすいクラス型についても併記します。

created

Vue.js

created は、インスタンス作成後、DOM へのマウント前に実行されます。

export default {
  created() {
    // ...
  }
}

React

React には明示的に created を指すライフサイクルはありません。

関数型コンポーネントでは、return 文より前が created にあたるでしょう。

関数型
function MyComponent() {
  // おそらくここが created にあたる

  return <div>...</div>;
}

クラス型では、クラスのコンストラクタが created にあたります。コンストラクタの引数は、後述するプロパティです。super に渡して呼び出す必要があります。

クラス型
class MyComponent extends React.Component {
  constructor(props) {
    super(props);

    // created
  }

  render() {
    return <div>...</div>;
  }
}

注意したいのは、Vue.js では API を介した外部データのフェッチは created で行うことが普通ですが、React では非同期なデータのフェッチはマウント後のタイミングで行うことが推奨されています。

mounted

Vue.js

mounted はインスタンスが DOM にマウントされた直後に呼ばれます。

export default {
  mounted() {
    // ...
  }
}

React

React の関数型コンポーネントでは、useEffect フックを使ってマウント後に実行したい処理を登録します。

関数型
import React, { useEffect } from 'react';

function MyComponent() {
  useEffect(() => {
    // マウント後に実行したい処理
    someApi.getItems().then(response => {
      // ...
    });
  }, []);

  return <div>...</div>;
}

正確には、useEffect はマウント後のライフサイクルについてだけ使われるフックではありませんuseEffect に登録した関数はレンダリングされるたびに呼び出されますが、第二引数によって挙動が異なります。

ちなみにコンポーネントは DOM に一度マウントされてから、一回以上レンダリング(描画)されます。レンダリングは、内部のステートや外部から渡されたプロパティ(後述)の変更に応じて発生します。

// 第二引数:なし
// 第一引数:レンダリング時に毎回呼ばれる
useEffect(() => {});

// 第二引数:空配列
// 第一引数:初回のレンダリング時のみ呼ばれる
// → つまり mounted と同等
useEffect(() => {}, []);

// 第二引数:中身のある配列
// 第一引数:配列で渡した変数のいずれかに変更があった場合のみ呼ばれる
useEffect(() => {}, [aaa, bbb]);

3つめの例では useEffect の第二引数に配列を渡しているため、aaabbb に変更がない場合は第一引数は実行されません。この動作は、監視プロパティ(watch)に似ていますね。

上記の例のように useEffect で非同期処理を行いたい場合、useEffect の第一引数である関数自体を async 関数にしてはいけません。

!!!
useEffect(async () => {
  const response = await someApi.getItems();
  // ...
}, []);

useEffect は何も返さないか、後述するクリーンアップ関数を返さなくてはいけない、という仕様だからです。async 関数は return しない場合でも暗黙的に Promise を返すので、この仕様に適合しません。

以下のように関数でラップしてやる必要があります。

useEffect(() => {
  const getItems = async () => {
    const response = await someApi.getItems();
    // ...
  };
  getItems();
}, []);

useLayoutEffect という useEffect と同種のフックも存在し、それぞれ下図のように実行タイミングが異なります。ほとんどの場合で useEffect を使用することが推奨されていますが、描画前に更新された DOM をもとにさらに見た目に関わる DOM 操作を行いたいレアケースに限って、useLayoutEffect を使用するのがよいようです(確実に描画前に実行されるため、中途半端な状態が一瞬見える「チラつき」を抑えられる)。

useLayoutEffect vs useEffect

クラス型では componentDidMount ライフサイクルメソッドを用います。

クラス型
class MyComponent extends React.Component {
  componentDidMount() {
    // ...
  }

  render() {
    return <div>...</div>;
  }
}

updated

Vue.js

updated はデータ更新により再描画が発生した後に呼ばれます。

export default {
  updated() {
    // ...
  }
}

React

再描画後の処理についても、useEffect を用います。ただし、useEffect は初回のレンダリング時にも呼ばれてしまうので、初回には呼ばれないようにするには useRef を用いた工夫が必要です。

関数型
import React, { useEffect, useRef } from 'react';

function MyComponent() {
  const mounted = useRef(false);

  useEffect(() => {
    if (!mounted.current) {
      mounted.current = true;
      return false;
    }

    // ...
  });

  return <div>...</div>;
}

useRef ではなく普通の変数を使ってしまうと、再描画時に関数自体が再実行される=その変数も再初期化されるので、うまく動きません(上の例でいうと再描画時には false に戻ってしまうため、以降の処理に進まない)。そこで useRef を用います。useRef はコンポーネントがマウントされている間、描画が複数回発生しても常に同じ値を返します。

クラス型では componentDidUpdate ライフサイクルメソッドを用います。

クラス型
class MyComponent extends React.Component {
  componentDidUpdate(prevProps, prevState, snapshot) {
    // ...
  }

  render() {
    return <div>...</div>;
  }
}

destroyed

Vue.js

destroyed はコンポーネントインスタンスが破棄されるときに呼び出されます。

export default {
  destroyed() {
    // ...
  }
}

React

React の関数型コンポーネントでは、useEffect の第一引数からクリーンアップ関数を返却できます。クリーンアップ関数はコンポーネントが UI から削除される前に呼び出されます。

関数型
import React, { useEffect } from 'react';

function MyComponent() {
  useEffect(() => {
    // ...

    return () => {
      // この関数はコンポーネントが UI から削除される前に呼び出される
    };
  });

  return <div>...</div>;
}

クラス型では componentWillUnmount ライフサイクルメソッドを用います。

クラス型
class MyComponent extends React.Component {
  componentWillUnmount() {
    // ...
  }

  render() {
    return <div>...</div>;
  }
}

errorCaptured

Vue.js

errorCaptured は、子孫コンポーネントでエラーが発生したときに呼び出されます。

export default {
  errorCaptured() {
    // ...
  }
}

React

クラス型では componentDidCatch ライフサイクルメソッドを用います。

クラス型
class MyComponent extends React.Component {
  componentDidCatch(error, info) {
    // ...
  }

  render() {
    return <div>...</div>;
  }
}

関数型のコンポーネントには、componentDidCatch に相当する機能はありません。同等のフックが用意されていないためです。

我々の目標はできるだけ早急にフックがすべてのクラスのユースケースをカバーできるようにすることです。まだ使用頻度の低い getSnapshotBeforeUpdate と componentDidCatch についてはフックでの同等物が存在していませんが、すぐに追加する予定です。

フックはクラスのユースケースのすべてをカバーしていますか?

エラー処理については、こちらの公式ブログ記事も参照してください。

ライフサイクルとフック

ライフサイクルをフックを用いて説明しましたが、厳密にはライフサイクルとフックは利用法が異なります。ライフサイクルメソッドは、文字通りライフサイクルの特定のタイミングごとに処理をまとめます。これに対してフックは機能ごとに処理をまとめます。

ライフサイクルメソッドとフックを同じ感覚で書くと、たとえば一つの useEffect の中に何種類かの独立した処理を書いてしまうかもしれません。

useEffect(() => {
  fetchUser().then(/* ... */);
  fetchItems().then(/* ... */);
}, []);

フックはもともと、クラス型では一つのライフサイクルメソッドに別種の処理のロジックがまとまってしまい、再利用性などを妨げる、という課題を解決するために出来た仕組みらしいです。そのため、独立した機能ごとにフックを分けるのがより良い書き方なのだと思います。

useEffect(() => {
  fetchUser().then(/* ... */);
}, []);

useEffect(() => {
  fetchItems().then(/* ... */);
}, []);

機能をカスタムフックに抽出すれば、よりクリーンになるかもしれません。

const user = useUser(userId);
const items = useItems();

コンポーネント間の連携

プロパティ

Vue.js

Vue.js では、親コンポーネントから子コンポーネントへのデータの受け渡しは props により実現します。

親コンポーネント
<Child :name="test" :item="sampleData" />
子コンポーネント
<script>
function Item(one, two) {
  this.one = one
  this.two = two
}

export default {
  props: {
    name: {
      type: String,
      required: false,
      default: 'John Doe'
    },
    item: {
      type: Item,
      required: true
    }
  }
}
</script>

props では typerequired などでプロパティのバリデーションを行えます。

React

React でも同様に、プロパティで親から子へデータを渡します。

親コンポーネント
<Child name={test} item={sampleData} />

関数型コンポーネントの第一引数にプロパティが渡されます。

子コンポーネント
import React from 'react'
import PropTypes from 'prop-types'

function Child(props) {
  // props.name
  // props.item
}

Child.propTypes = {
  name: PropTypes.string,
  item: PropTypes.shape({
    one: PropTypes.string.isRequired,
    two: PropTypes.number.isRequired
  }).isRequired
}

Child.defaultProps = {
  name: 'John Doe'
}

export default Child

プロパティのバリデーションには prop-types というライブラリを用います。

イベント通知

Vue.js

Vue.js では $emit メソッドで親にイベントを通知します。

子コンポーネント
onSomethingHappened() {
  this.$emit('hello');
}

通知されたイベントには、v-on(または省略形の @)でイベントハンドラを登録します。

親コンポーネント
<Child @hello="parentMethod" />

React

React には、イベント独自の構文は存在しません。プロパティとして関数を渡すことで、子のイベントに対する処理を親がハンドリングします。

子コンポーネント
function Child(props) {
  const onSomethingHappened = () => {
    props.onHello();
  };
}
親コンポーネント
<Child onHello={parentMethod} />

コンテンツ差し込み

Vue.js

Vue.js には、slot によるコンテンツ差し込み機能が備わっています。

親コンポーネント
<Child>
  <p>Hello world</p>
</Child>
子コンポーネント
<template>
  <div>
    <slot></slot>
  </div>
</template>

React

React にもコンテンツの差し込み機能が存在します。

親コンポーネント
<Child>
  <p>Hello world</p>
</Child>

プロパティの children にコンテンツが格納されています。

子コンポーネント
function Child(props) {
  return <div>{props.children}</div>
}

複数のコンテンツ差し込み

Vue.js

Vue.js で複数のコンテンツを差し込む場合は、それぞれのブロックに名前をつけることができます。

<MyComponent>
  <template #header>
    <MyHeader />
  </template>
  <template #content>
    <MyContent />
  </template>
  <template #footer>
    <MyFooter />
  </template>
</MyComponent>

React

React には children を複数指定できませんし、名前をつけることもできません。

ただ、children は単にプロパティの一つでしかありません。そしてプロパティにはコンポーネントそのものを設定することも可能です。つまり、以下のように複数のコンポーネントを直接プロパティとして渡してやれば OK です。

親コンポーネント
return (
  <MyComponent
    header={<MyHeader />}
    content={<MyContent />}
    footer={<MyFooter />}
  />
);
子コンポーネント
function MyComponent(props) {
  return (
    <div>
      <header>{props.header}</header>
      <main>{props.contnet}</main>
      <footer>{props.footer}</footer>
    </div>
  )
}

おわりに

以上、Vue.js エンジニアの視点から、React の機能をまとめました。

本記事を書いてみて、Vue.js と比べて React はライブラリの機能がシンプルにできていると感じました。

Vue.js は computed や watch、様々なテンプレート構文などを備えていますが、React は基本的にコアな機能だけを提供して、それらと JavaScript そのもので工夫する、フレームワーク的な、親切な(良くも悪くも)機能を追加しない作りになっていますね。

そのためか、とっつきやすさ、パッと見のコードの分かりやすさ、それなりの開発経験があれば何やってるかなんとなく分かる感じは、やはり Vue.js に軍配が上がります。

React のシンプルさは、使い込むと居心地が良くなるのかもしれません。いずれにせよ、ひとつのフレームワークに執着する必要はありませんから、隙を見て実務でももっと使っていこうと思います。