31
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

2022年版 React, Vue, Angularの比較

Last updated at Posted at 2021-12-31

2022年に備えての備忘録を残します。

TypeScriptを使わない理由はないのでTypeScript前提です。

記事の目標としては初心者が3種を比較してそれっぽいアプリケーションを作成するまでのハンズオン用の資料として使えることです。

アプリの初期化

React アプリの初期化

$ npx create-react-app my-app-name --template typescript

Angular アプリの初期化

$ ng new my-app-name

Vue アプリの初期化

$ vue create my-app-name

まとめ

  • Reactはウィザード形式ではないので、TypeScript
  • AngularはTypeScriptが強制される
  • vueはウィザードでTypeScriptを選ぶことができる

開発サーバー&ビルド

React 開発サーバー起動

$ npm run start
-> localhost:3000

Angular 開発サーバー起動

$ npm run serve
-> localhost:4200

Vue 開発サーバー起動

$ npm run serve
-> localhost:8080

Sassの利用

React

$ npm install --save-dev node-sass
$ touch style.module.scss

Sassの利用方法は2種類

  1. ファイルとしてインポートする
import './App.scss';
  1. モジュール化する
import styles from "./style.module.scss";
return <div className={styles.app_root}>...</div>

Angular

  • 初期化時にSCSSを選んでおけばそのまま利用できる
  • app.component.scssが初期状態で入っている

Vue

$ npm install --save-dev node-sass@6 sass-loader@10
  • 初期ウィザードでCSS Preprocessorを選択していればそのまま使える
  • 後から追加でSassを利用するにはsass-loaderを入れる必要があるが、vueコマンドでインストールされるwebpackのバージョンが影響するため、古いバージョンをインストールする必要がある
<style lang="scss">
.hello {
  p{
     color:red;
  }
}
</style>

lang属性で指定することで各コンポーネント毎にsassを書くことができる。

まとめ

  • 標準でサポートしてくれるAngularは初心者向き
  • Reactのモジュールの形式はクラス名が自動で作られるのがありがたい
  • Vueは全て1ファイルに書ける以外いいところが無い

コンポーネントの作成

React

$ touch src/MyComponent.tsx
export const MyComponent = () => {
   return <p>MyComponent</p>
}

呼び出し側

import { MyComponent } from './MyComponent';
<MyComponent />
  • コンポーネント作成のコマンドなどは無い
  • 関数コンポーネント以外にもクラスコンポーネントがある
  • コンポーネント名は関数名(関数コンポーネントの場合)

Angular

$ ng generate component Child
import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-child',
  templateUrl: './child.component.html',
  styleUrls: ['./child.component.scss']
})
export class ChildComponent implements OnInit {

  constructor() { }

  ngOnInit(): void {
  }

}

呼び出し側

<app-child></app-child>
  • 作成コマンドがある
  • 自動で読み込まれる(app.modules.tsが自動で更新される)ため、呼び出し側はimportなどが不要
  • コンポーネント名は@Componentで指定したもの

Vue

$ touch src/components/MyComponent.vue
<template>
  <p>My Component</p>
</template>

<script>
export default {};
</script>

<style scoped lang="scss">
p {
    color:black;
}
</style>

呼び出し側

import MyComponent from './MyComponent.vue';
<my-component />
  • コマンドなどは無いのでファイルは自分で作る必要がある
  • 1ファイルにすることができるが、分けて作ることも可能
  • コンポーネント名はファイル名をスネークケースに変えたもの

まとめ

  • Angularは全部盛りなのがありがたい
  • Reactが超軽量で使い勝手が良い
  • Vueは1ファイルになる以外特に良い所が無い

Stateの利用(カウンターアプリケーションの作成)

  • Stateの利用方法を確認するために簡単なアプリケーションを作成してみる
  • カウンター表示コンポーネント、Incrementコンポーネント、Decrementコンポーネントの3つのコンポーネントで一つのStateを共有する

Reactでのカウンターアプリの作成(Redux無し)

$ touch src/counter/Countetr.tsx
$ touch src/counter/CounterDisplay.tsx
$ touch src/counter/CounterIncrement.tsx
$ touch src/counter/CounterDecrement.tsx
Countetr.tsx
import { useState } from "react"
import { CounterDecrement } from "./CounterDecrement"
import { CounterDisplay } from "./CounterDisplay"
import { CounterIncrement } from "./CounterIncrement"

export const Counter = () => {

	const [count, setCount] = useState(0)

	return <div>
		<h1>Counter</h1>
		<CounterDisplay count={count}></CounterDisplay>
		<CounterDecrement onclick={ () => { setCount(count-1) } } ></CounterDecrement>
		<CounterIncrement onclick={ () => { setCount(count+1) } }></CounterIncrement>

	</div>
}
CounterDisplay.tsx
export const CounterDisplay = ( props : { count: number }) => {
	return <div>
		<p>{ props.count }</p>
	</div>
}
CounterDisplay.tsx
export const CounterDisplay = ( props : { count: number }) => {
	return <div>
		<p>{ props.count }</p>
	</div>
}
CounterDecrement.tsx
export const CounterDecrement = (props: { onclick:() => void }) => {
	return <div>
		<button onClick={ props.onclick }> - </button>
	</div>
}
CounterIncrement.tsx
export const CounterIncrement = (props: { onclick:() => void }) => {
	return <div>
		<button onClick={ props.onclick }> + </button>
	</div>
}
  • シンプルで簡単に作れる
  • ルーティング機能が標準では無い
  • イベントやプロパティの渡し方が直感的

Angularでのカウンターアプリの作成

$ ng g m Counter
$ ng g c counter/Counter
$ ng g c counter/CounterDisplay
$ ng g c counter/CounterIncrement
$ ng g c counter/CounterDecrement
$ ng g m counter/CounterRouting --flat
counter-routing.module.ts
import { NgModule } from '@angular/core';
import { CounterComponent } from './counter/counter.component';
import { RouterModule, Routes } from '@angular/router';

const routes: Routes = [
  {
    path: '',
    component: CounterComponent,
  },
];

@NgModule({
  declarations: [],
  imports: [RouterModule.forChild(routes)],
})
export class CounterRoutingModule {}
counter.module.ts
import { NgModule } from '@angular/core';
import { CounterComponent } from './counter/counter.component';
import { CounterDisplayComponent } from './counter-display/counter-display.component';
import { CounterIncrementComponent } from './counter-increment/counter-increment.component';
import { CounterDecrementComponent } from './counter-decrement/counter-decrement.component';
import { CounterRoutingModule } from './counter-routing.module';

@NgModule({
  declarations: [
    CounterComponent,
    CounterDisplayComponent,
    CounterIncrementComponent,
    CounterDecrementComponent,
  ],
  imports: [CounterRoutingModule],
})
export class CounterModule {}

Counterコンポーネント

counter.component.html
<app-counter-display [count]="count"></app-counter-display>
<app-counter-increment (event)="setCount(+1)"></app-counter-increment>
<app-counter-decrement (event)="setCount(-1)"></app-counter-decrement>
counter.component.ts
import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-counter',
  templateUrl: './counter.component.html',
  styleUrls: ['./counter.component.scss'],
})
export class CounterComponent implements OnInit {
  count: number = 0;

  constructor() {}

  ngOnInit(): void {}

  setCount(value: number) {
    this.count += value;
  }
}

CounterDisplayコンポーネント

counter-display.component.html
<p>{{ count }}</p>
counter-display.component.ts
import { Component, Input, OnInit } from '@angular/core';

@Component({
  selector: 'app-counter-display',
  templateUrl: './counter-display.component.html',
  styleUrls: ['./counter-display.component.scss'],
})
export class CounterDisplayComponent implements OnInit {
  @Input() count: number = 0;

  constructor() {}

  ngOnInit(): void {}
}

CounterIncrementコンポーネント

counter-increment.component.html
<button (click)="onClick()"> + </button>
counter-increment.component.ts
import { Component, OnInit, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'app-counter-increment',
  templateUrl: './counter-increment.component.html',
  styleUrls: ['./counter-increment.component.scss'],
})
export class CounterIncrementComponent implements OnInit {
  @Output() event = new EventEmitter();

  constructor() {}

  ngOnInit(): void {}

  onClick() {
    this.event.emit('increment');
  }
}

CounterDecrementコンポーネント

counter-decrement.component.html
<button (click)="onClick()"> - </button>
counter-decrement.component.ts
import { Component, OnInit, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'app-counter-decrement',
  templateUrl: './counter-decrement.component.html',
  styleUrls: ['./counter-decrement.component.scss'],
})
export class CounterDecrementComponent implements OnInit {
  @Output() event = new EventEmitter();

  constructor() {}

  ngOnInit(): void {}

  onClick() {
    this.event.emit('decrement');
  }
}

ルーティングの設定

app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

const routes: Routes = [
  {
    path: 'counter',
    loadChildren: () => {
      return import('./counter/counter.module').then((m) => {
        return m.CounterModule;
      });
    },
  },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
})
export class AppRoutingModule {}
  • ファイルが多い
  • コマンドで作れるが「flatオプション」やルーティングモジュールなどの専用のものが必要になるので利便性<面倒くささを感じる
  • ルーティングが標準だが、複雑に思える。一回使えばなんとかなりそう
  • イベントの渡し方も@Input@Outputがあり難しい。それぞれの書き方が()と[]で違う部分が混乱する
  • プロパティを更新 = Stateの更新っは直感的でわかりやすい
  • 覚えたらなんでもできる強みがある

Vueでのカウンターアプリの作成

$ touch src/views/Counter.vue
$ touch src/components/counter/CounterDisplay.vue
$ touch src/components/counter/CounterIncrement.vue
$ touch src/components/counter/CounterDecrement.vue
Counter.vue
<template>
  <div class="counter">
	  <p>Counter</p>
	  <CounterDisplay :count="count"></CounterDisplay>
	  <CounterIncrement @increment="incrementNumber"></CounterIncrement>
	  <CounterDecrement @decrement="decrementNumber"></CounterDecrement>
  </div>
</template>

<script lang="ts">
import CounterDisplay from '@/components/counter/CounterDisplay.vue';
import CounterIncrement from '@/components/counter/CounterIncrement.vue';
import CounterDecrement from '@/components/counter/CounterDecrement.vue';

import { defineComponent, ref } from 'vue';

export default defineComponent({
  name: 'Counter',
  components: {
    CounterDisplay,
	CounterIncrement,
	CounterDecrement
  },
  setup(){

	  const count = ref(0)

	  const decrementNumber = () => {
		  count.value = count.value - 1
	  }

	  const incrementNumber = () => {
		  count.value = count.value + 1
	  }

	  return { count, decrementNumber, incrementNumber };

  }
});
</script>
CounterDisplay.vue
<template>
  <p>count={{ count }}</p>
</template>

<script lang="ts">
import { defineComponent } from 'vue';

export default defineComponent({
  name: 'CounterDisplay',
  props: {
	count:Number
  }
});
</script>
CounterIncrement.vue
<template>
  <button @click="onClickButton"> + </button>
</template>

<script lang="ts">
import { defineComponent } from 'vue';

export default defineComponent({
  name: 'CounterIncrement',
  setup(_,context){
	  const onClickButton = () => {
		  context.emit("increment")
	  }

	  return { onClickButton }
  }
});
</script>
CounterDecrement.vue
<template>
  <button @click="onClickButton"> - </button>
</template>

<script lang="ts">
import { defineComponent } from 'vue';

export default defineComponent({
  name: 'CounterDecrement',
  setup(_,context){
	  const onClickButton = () => {
		  context.emit("decrement")
	  }
	  
	  return { onClickButton }
  }
});
</script>
router/index.ts
const routes: Array<RouteRecordRaw> = [
	....
	{
		path: "/counter",
		name: "Counter",
		component: Counter,
	},
];
  • composition APIはReactのパクリだけどdataやpropsなどもあるせいで、結局複雑になってる
  • ルーティングが標準であるのは使いやすいしシンプルに書ける
  • 双方向のSateをやるためにref()を使うのはReactと似ている

まとめ

  • VueはReactをパクリつつまじでどっちつかずの方針になっているので今後採用はしない
  • Reactはシンプルで良い。ルーティングは無いが役割をViewに限定しているので良い。
  • Angularは全部盛り。理解出来たらすごく強い。

愚痴

Vue2使ってましたが、Vue3がしんどいなぁ移行嫌だなぁを可視化するためにやりました。
TypeScriptへの相性悪くなってないですか…?ClassComponentじゃなくてCompositionAPIを使うのが正しい進化だと思うのだけど、使いにくくなってるだけに思える…。

シンプルじゃなくなってるし、書き方が色々ありすぎるし、やればやるほどVueへの不満が溜まってきて悲しいです。

でももう多分無理です。さようならVue。Vue2 -> ReactかVue2 -> Angularどっちかにしようと思います。

31
9
8

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
31
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?