この記事では、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 にもありますね。created
や mounted
です。)
しかしバージョン 16.8 で関数型コンポーネントのためのフックという機能が追加され、関数型コンポーネントでも状態管理やライフサイクルの機能にアクセスできるようになりました。
まだ(v16.9.0時点)フックは、クラス型の持つ機能を完全に再現できていません。しかし React 開発チームの展望としては、フックへの機能追加を積極的に行い、関数型をメインの方法にしてもらいたいと考えているようです。
長期的には、フックが React のコンポーネントを書く際の第一選択となることを期待しています。
これから学習や開発を始める場合は、まず関数型で書いて、必要な場合のみクラス型にする、という方針になるかと思います。いまのところクラス型が必要になるのは、フックでサポートされていないライフサイクルにアクセスしたい場合でしょう。
本記事では、関数型コンポーネントをメインに説明していきます。
テンプレート
Vue.js
Vue.js の <template>
には、v-bind
や v-for
、v-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 の拡張なので、
if
やfor
などの独自の制御構文は存在しません。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 では class
とv-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
の第二引数に配列を渡しているため、aaa
と bbb
に変更がない場合は第一引数は実行されません。この動作は、監視プロパティ(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
を使用するのがよいようです(確実に描画前に実行されるため、中途半端な状態が一瞬見える「チラつき」を抑えられる)。
クラス型では 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
では type
や required
などでプロパティのバリデーションを行えます。
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 のシンプルさは、使い込むと居心地が良くなるのかもしれません。いずれにせよ、ひとつのフレームワークに執着する必要はありませんから、隙を見て実務でももっと使っていこうと思います。