2022年版 React, Vue, Angularの比較

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';
```

2. モジュール化する

```
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
```

```tsx
export const MyComponent = () => {
   return <p>MyComponent</p>
}
```

呼び出し側

```tsx
import { MyComponent } from './MyComponent';
<MyComponent />
```

- コンポーネント作成のコマンドなどは無い
- 関数コンポーネント以外にもクラスコンポーネントがある
- コンポーネント名は関数名（関数コンポーネントの場合)


### Angular

```
$ ng generate component Child
```

```ts
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
```

```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>
}
```

```tsx:CounterDisplay.tsx
export const CounterDisplay = ( props : { count: number }) => {
	return <div>
		<p>{ props.count }</p>
	</div>
}
```

```tsx:CounterDisplay.tsx
export const CounterDisplay = ( props : { count: number }) => {
	return <div>
		<p>{ props.count }</p>
	</div>
}
```

```tsx:CounterDecrement.tsx
export const CounterDecrement = (props: { onclick:() => void }) => {
	return <div>
		<button onClick={ props.onclick }> - </button>
	</div>
}
```

```tsx: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
```

```ts: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 {}
```

```ts: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コンポーネント

```html: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>
```

```ts: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コンポーネント

```htmｌ:counter-display.component.html
<p>{{ count }}</p>
```

```ts: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コンポーネント

```html:counter-increment.component.html
<button (click)="onClick()"> + </button>
```

```ts: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コンポーネント

```html:counter-decrement.component.html
<button (click)="onClick()"> - </button>
```

```ts: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');
  }
}
```

ルーティングの設定

```ts: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
```

```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>
```

```vue:CounterDisplay.vue
<template>
  <p>count={{ count }}</p>
</template>

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

export default defineComponent({
  name: 'CounterDisplay',
  props: {
	count:Number
  }
});
</script>
```

```vue: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>
```

```vue: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>
```

```ts: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どっちかにしようと思います。