ReactとAngular 2の比較(React vs. Angular 2)

2016年9月16日に長らく開発が進められていたAngular 2の正式版がリリースされました。自分はどちらかというとReactの人間なので、Angular 2に関してはまだまだ知らないことが多いです。そんな折、TwitterかFacebookを見ていた時に自分のタイムラインに「React vs. Angular 2 | React vs. Angular 2 | hack.guides()」という実に興味深いタイトルの記事の情報が流れてきました。内容はともかく、コードの比較などもあり、しっかり読んでみたかったので、日本語に訳してみることにしました。特にAngular 2の部分が知識不足でうまく訳せてない部分もあるかと思いますが、ぜひ参考に読んでみてください。

React vs Angular 2

当記事は、React vs. Angular 2 | hack.guides() by Hristo Georgievを日本語に翻訳したものです。

この記事では2つの有名なフロントエンドのWeb開発用ツールReactAngular 2の批評的概要を紹介していきます。これらのツールはアプリケーション構造の組み立て方、コミュニティによる採用、パフォーマンス、異なったプラットフォームを統合できる点などに基づいて高く評価されています。

この記事のゴールはこの2つのツールの主な相違点を明確にし、類似点を理解することとしています。従って、与えられたプロジェクトの仕様書にどちらのツールが適切か選ぶことになった際に、意思決定のプロセスが容易になります。

Development flow

Angular 2

Angular 2はすでに十分に成熟したフレームワークになってきています。Webアプリケーションの開発における工程のほとんどをカバーしてくれる多くの機能が最初から備わっています。また、要素の役割も以下のように明確にわけられています。

  • Services – APIからのデータを使用するため、または複数のコンポーネント間で状態をシェアするために使われるインジェクト用の要素です。
  • Components – サービスを使用するユーザーインターフェースの構成要素です。structural directiveのselectorを通して、互いに内部にネストすることができます。
  • Directives – structural directivesとattribute directivesに分けられます。structural directives(例えば*ngfor)はDOM(Document Object Model)を操作します。一方attribute directivesは要素の一部となり、それらのスタイルや状態をコントロールします。
  • Pipes – viewレイヤーにおいてどのようにデータが表示されるかフォーマットするために使われます。

React

ReactはAngular 2と比べると若干柔軟性に欠けます。フレームワークというよりはWebアプリケーションを構築するための基本的なツール(HTTP serviceとComponents)をメインに提供するライブラリです。ルーティングの機能など特定の用途に使われる機能は含まれていません。アプリケーションを構想するためにどんなパッケージを使うかは全て開発者の選択次第となっています。

Code structure

Angular 2

Angular 2はMicrosoftによって開発されているJavaScriptのsupersetであるTypeScriptで書かれています。この言語は型、新しいデータ構造、拡張されたオブジェクト指向の機能を備え、素のJavaScriptに比べてコードが読みやすく、メンテナスしやすいようになっています。ただ、TypeScriptの構文はC#を連想させるものであり、標準的なJavaScriptからの移行はやや難しいでしょう。余計な環境設定のオーバーヘッドが含まれており、TypeScriptを使うことはAngular 2アプリケーションのセットアップをどうにもつまらないものにするかもしれません。

React

ReactはJSX(Java Serialization to XML / JavaScriptやHTMLをレンダリングするためのXML風な拡張構文)に依存します。構文や構造の面では、JSXはJavaScriptのように見え、簡単にHTMLの中に溶け込んでいるように見えます。ReactとAngular 2のコードの構造についてより明確な比較をするために、基本的なTODOアプリケーションがどのように作られているかを見て比較していきます。(サンプルはMark Volkmann (mvolkmann)氏によって作られています。)

どちらのアプリケーションも開発やデプロイ用にWebpackを使います。

index.html

Angular 2
<!DOCTYPE html>
<html>
  <head>
    <title>Angular Todo App</title>
    <link rel="stylesheet" href="todo.css">
  </head>
  <body>
    <todo-list>Loading...</todo-list>
    <script src="bundle.js"></script>
  </body>
</html>
React
<!DOCTYPE html>
<html>
  <head>
    <title>Angular Todo App</title>
  </head>
  <body>
    <todo-list>Loading...</todo-list>
    <script src="bundle.js"></script>
  </body>
</html>

どちらも全てのアセットを扱うのにWebpackを使用するような設定となっているので、index.htmファイルはほぼ同じのものとなります。

アプリケーション構造の面では、ReactもAngular 2も2つのファイル(タスクのリストを表示するTodoListとシングルタスクを表示するTodo)を必要とします。

TodoListコンポーネント(Angular 2はtodolist.component.tsファイル、Reactはtodo-list.jsファイル)をひとつひとつ上から下まで分析してみましょう。

Importing

Angular 2
import {Component} from '@angular/core';
import {bootstrap} from from '@angular/platform-browser-dynamic';;
import {TodoCmp, ITodo} from "./todoCmp";
React
import React from 'react';
import ReactDOM from 'react-dom';
import Todo from './todo';

Initializing a component

ツール間の重要な違いについては、コメントをつけています。

Angular 2
// TodoListCmp classのstateプロパテティの型のために使われます。
// どちらのプロパティも任意ですが、それらはよりコードをメンテナブルにするため
// 好まれます。
interface IState {
  todos?: ITodo[];
  todoText?: string;
}

// この構文はDecoratorと呼ばれていて、他の言語における
// Annotationsと同様のものです。DecoratorはES2016に含まれるか
// 考慮されているところです。
@Component({
  // 他のコンポーネントはここで使われます。
  directives: [TodoCmp],
  // TodoListCmpのHTML要素のselector
  selector: 'todo-list',
  template: // ここの値は下のテンプレートの部分を参照してください。
})

export class TodoListCmp {
  private static lastId: number = 0;
  private state: IState;

  constructor() {
    this.state = {
      todos: [
        TodoListCmp.createTodo('learn Angular', true),
        TodoListCmp.createTodo('build an Angular app')
      ]
    };
  }
React
let lastId = 0; // ES6の静的なclassのプロパティはありません

class TodoList extends React.Component {
  constructor() {
    super(); // "this"にアクセスする前に呼び出す必要があります
    this.state = {
      todos: [
        TodoList.createTodo('learn React', true),
        TodoList.createTodo('build a React app')
      ]
    };
  }

Creating an item

Angular 2
static createTodo(
    text: string, done: boolean = false): ITodo {
    return {id: ++TodoListCmp.lastId, text, done};
}

onAddTodo(): void {
    const newTodo: ITodo =
      TodoListCmp.createTodo(this.state.todoText);
    this.state.todoText = '';
    this.state.todos = this.state.todos.concat(newTodo);
}
React
static createTodo(text, done = false) {
    return {id: ++TodoList.lastId, text, done};
}

onAddTodo() {
    const newTodo =
      TodoList.createTodo(this.state.todoText);
    this.setState({
      todoText: '',
      todos: this.state.todos.concat(newTodo)
    });
}

上記のように、創作過程は両方のツールともかなり似ています。しかしReactの方が若干コード量も少なく、開発ロジックも少なくて済むように見えます。

Detecting item states

Angular 2
get uncompletedCount(): number {
    return this.state.todos.reduce(
      (count: number, todo: ITodo) =>
        todo.done ? count : count + 1, 0);
}
React
get uncompletedCount() {
    return this.state.todos.reduce(
      (count, todo) =>
        todo.done ? count : count + 1, 0);
}

Reacting to changes

Angular 2
onArchiveCompleted(): void {
    this.state.todos =
      this.state.todos.filter((t: ITodo) => !t.done);
}

onChange(newText: string): void {
    this.state.todoText = newText;
}

onDeleteTodo(todoId: number): void {
    this.state.todos = this.state.todos.filter(
      (t: ITodo) => t.id !== todoId);
}

onToggleDone(todo: ITodo): void {
    const id: number = todo.id;
    this.state.todos = this.state.todos.map(
      (t: ITodo) => t.id === id ?
        {id, text: todo.text, done: !todo.done} : t);
}
React
onArchiveCompleted() {
    this.setState({
      todos: this.state.todos.filter(t => !t.done)
    });
}

onChange(name, event) {
    this.setState({[name]: event.target.value});
}

onDeleteTodo(todoId) {
    this.setState({
      todos: this.state.todos.filter(
        t => t.id !== todoId)
    });
}

onToggleDone(todo) {
    const id = todo.id;
    const todos = this.state.todos.map(t =>
      t.id === id ?
        {id, text: todo.text, done: !todo.done} :
        t);
    this.setState({todos});
}

Bootstrapping

Angular 2
} // TodoListCmp classはここまで

// それぞれのAngularアプリケーションははっきりとルートコンポーネントを
// 明示するためにbootstrapの呼び出しを必要とします。
// より大きなアプリケーションでは、bootstrappingは通常
// より多くの設定を伴った別ファイルの中にあります。
bootstrap(TodoListCmp);
React
} // TodoList classはここまで

// これはTodoListコンポーネントを
// 特定のDOM要素の中にレンダリングします。
// もしTodoListが1箇所以上で使われていたとしたら、
// これは異なったJavaScriptファイルに移動されるでしょう。
ReactDOM.render(
  <TodoList/>,
  document.getElementById('container'));

Template syntax

Angular 2
  // これは上記の@Componentテンプレートのプロパティの値です。
  // Reactのrenderメソッドと比べると簡単にここに移動できました。
  `<div>
    <h2>To Do List</h2>
    <div>
      {{uncompletedCount}} of
      {{state.todos.length}} remaining
      <!-- Clicking this button invokes the
           component method onArchiveCompleted. -->
      <button (click)="onArchiveCompleted()">
        Archive Completed
      </button>
    </div>
    <br/>
    <form>
      <!-- [ngModel] tells where to get the value. -->
      <!-- (input) tells what to do on value change. -->
      <input type="text" size="30" autoFocus
        placeholder="enter new todo here"
        [ng-model]="state.todoText"
        (input)="onChange($event.target.value)"/>
      <button [disabled]="!state.todoText"
        (click)="onAddTodo()">
        Add
      </button>
    </form>
    <ul>
      <!--
        This uses a for loop to generate TodoCmps.
        #todo defines a variable to use within the loop.
        [todo] sets a property on each TodoCmp.
        (onDeleteTodo) and (onToggleDone) are outputs
        from the TodoCmp, and they call functions
        on TodoListCmp with emitted values.
        deleteTodo receives a type of number.
        toggleDone receives a type of ITodo.
      -->
      <todo *ngFor="let todo of state.todos" [todo]="todo"
        (on-delete-todo)="onDeleteTodo($event)"
        (on-toggle-done)="onToggleDone($event)"></todo>
    </ul>
  </div>
React
render() {
    // 生成されたUIの一部を変数に割り当て、後ほど参照できます。
    const todos = this.state.todos.map(todo =>
      <Todo key={todo.id} todo={todo}
        onDeleteTodo=
          {this.onDeleteTodo.bind(this, todo.id)}
        onToggleDone=
          {this.onToggleDone.bind(this, todo)}/>);

    return (
      <div>
        <h2>To Do List</h2>
        <div>
          {this.uncompletedCount} of
          {this.state.todos.length} remaining
          <button
            onClick={() => this.onArchiveCompleted()}>
            Archive Completed
          </button>
        </div>
        <br/>
        <form>
          <input type="text" size="30" autoFocus
            placeholder="enter new todo here"
            value={this.state.todoText}
            onChange=
              {e => this.onChange('todoText', e)}/>
          <button disabled={!this.state.todoText}
            onClick={() => this.onAddTodo()}>
            Add
          </button>
        </form>
        <ul className="unstyled">{todos}</ul>
      </div>
    );
 }

Angular 2のコンポーネントのコードは一般的にReactに比べると冗長です。ReactがAngular 2より多くのコードを必要とする唯一のケースはbootstrappingの箇所のみです。この不釣り合いはTypeScriptに起因します。:void, :ITodoや他の型はしばしばTypeScriptで見られます。Angular 2のスタイルでは、変数、関数の引数、そして関数はそれぞれ自身がTypeScriptの型を持たなくていけないと通常規定されています。Reactはそういったものは持ちません。伝統的でより簡潔なJavaScriptを好む開発者のためにコードをより扱いやすくしています。

Angular 2は、自身が含んでいる異なる構成(services, directives, pipes)を明示するために、そして独自のプロパティを定義するために@Componentデコレータを使用します。それはまた開発者にコンポーネントのためにselectorを明示させ、もし別のファイルが使われた場合、templateのプロパティかtemplateUrlを使っているテンプレートのためにコードを置かせます。同じことがstylesにも当てはまります - stylesのプロパティを使いながら直接スタイルをインプットするためか、styleUrlsを使いながらスタイルのファイルの配列をインクルードするためのオプションがあります。Reactはコンポーネントの装飾のためにそういったものは持たず、コンポーネントのファイルにインクルードするためにコンポーネントのテンプレートを必要とします。このようにして、カスタマイズのためのオプションを制限し、コンポーネントに関する情報を解釈することをより難しくしています。

Angular 2のテンプレートは異なった要素のために特定の慣習を含んでいます。

  • *はDOMを操作するディレクティブを意味します。例えば*ngForは値の配列を繰り返し、それぞれ新しい要素を作ります。*ngIfは要素があるかないか示すために使われます。
  • []はバインドしているプロパティを意味します。要素がコンポーネントからの値を受け取るということを示しています。
  • ()(click)のようなバインドしているイベントを示します。コンポーネントにおける関数のトリガーとなります。
  • [()]は双方向バインディングを示します。渡されたデータに対するどんな変更も伝播されないということを意味します。

Reactのテンプレートもまた独自性を含んでいます。JSXは、独自のマークアップによるHTMLやJavaScriptでコードを曖昧にします。テンプレートはHTMLで書かれているように見えますが、マークアップは実際には後でHTMLにパースされるXMLです。JSXでは、標準的なHTMLのプロパティのワードはキャメルケースで書かれるか、またはJSXでのonclickonClickとなるように異なった名称を使います。for(label要素で使われる)はJavaScriptでの予約語(繰り返しのfor)となっているためhtmlForになります。

Nesting components

次に、シングルToDoアイテムのコンポーネントがAngular 2とReactの中でどのようになっているか見てみましょう。

Angular 2
import {Component, Input, Output, EventEmitter}
  from 'angular2/angular2';

export interface ITodo {
  id: number;
  text: string;
  done: boolean;
}

@Component({
  selector: 'todo',
  template: `
    <li>
      <input type="checkbox"
        [checked]="todo.done"
        (change)="toggleDone()"/>
      <span [ng-class]="'done-' + todo.done">
        {{todo.text}}
      </span>
      <button (click)="deleteTodo()">Delete</button>
    </li>`
})
export class TodoCmp {
  // @Inputはこのコンポーネントが含まれている
  // コンポーネントから初期値を受け取ることを許可します。
  @Input() todo: ITodo;
  // @Onputはこのコンポーネントが含まれている
  // コンポーネントに値を発行することを許可します。
  @Output() onDeleteTodo:
    EventEmitter<number> = new EventEmitter<number>();
  @Output() onToggleDone:
    EventEmitter<ITodo> = new EventEmitter<ITodo>();

  deleteTodo(): void {
    this.onDeleteTodo.next(this.todo.id);
  }

  toggleDone(): void {
    this.onToggleDone.next(this.todo);
  }
}
React
import React from 'react';

// Reactのコンポーネントを定義する方法は3つあります。
// これは"props"を通してのみデータを受け取る
// stateless function component formです。
// propsオブジェクトはこの関数に渡され、分解されます。
const Todo = ({onDeleteTodo, onToggleDone, todo}) =>
  <li>
    <input type="checkbox"
      checked={todo.done}
      onChange={onToggleDone}/>
    <span className={'done-' + todo.done}>
      {todo.text}
    </span>
    <button onClick={onDeleteTodo}>Delete</button>
  </li>;

// 任意のpropsオブジェクトのバリデーション
const PropTypes = React.PropTypes;
Todo.propTypes = {
  todo: PropTypes.object.isRequired,
  onDeleteTodo: PropTypes.func.isRequired,
  onToggleDone: PropTypes.func.isRequired
};

export default Todo;

コンポーネントはReactアプリケーションの重要な構成要素であるため、構築する方法にはより大きな柔軟性が提供されています。stateless componentは一方向のデータフローを考慮して構築されています。ロジックの大部分はコンポーネントの外に置かれます。Angular 2もまた同様な機能を提供していますが、それを行うには若干複雑となります。

Application Architecture

Angular 2

angular 2 data flow
Bi-directional data flow typical for MVC patterns

Angular 2は標準的な双方向性のデータフローモデルを使用しています。actionが一度作られると、データはサービスレイヤーに上がって行きます。その時にデータはviewに戻ってきます。このようなデータの操作方法は広く批判されています。なぜならデータの状態はどちらからでも変更される可能性があり、結果として矛盾が生じるからです。

React

Flux data flow
Flux data flow

Reactはそのアプリケーションを構築するためにFluxを使います。それぞれのactionはdispatcherに送られます。dispatcherは新しい情報をstoreに送ります。storeはその時情報をアップデート(mutate)し、view(controller-views)に送り出します。viewは変更を監視しています。

Redux

 Redux data flow
Redux data flow

Reduxはかなりイケてます。Fluxのように、一方向性のデーターフローをベースとしていますが、Fluxとは異なった方法でデータを管理します。

Reduxを使うアプローチは、アプリケーション全体を通してシェアされるグローバルなstateを持ちます。actionが作られた時、dispatcherに送られます。dispatcherはAPIに対してHTTP呼び出しを作成します。その時、変更はreducerに送られます。reducerは純粋な関数で、グローバルなアプリケーションのstateの一部を管理することを専門とします。例えば、あなたのアプリケーションがmodelとしてuser, project, itemを持っていたとしたら、modelのそれぞれはグローバルなstateの一部となり、stateを管理するreducerに渡されるようになります。reducerによって一度stateが変更(mutate)されると、新しい情報はviewに送り出されます。

このように、アプリケーションのstateはひとつの場所で保存されます。stateの管理がより簡単になっています。

Reduxは最初シンプルなFluxの代替ツールとしてReactで使われましたが、このような理由により、Reactのコミュニティにおいてより広く採用されるようになっています。

最近、Angular 2は、dataの状態をメンテナンスするためにrxjsのobservablesを利用し、dataの状態を操作するために純粋な関数を使用することで、データフローにReduxパターンを採用し始めています。Angular 2のアプリケーションで一方向のデータフローを構築するために試みられていますが、この技術の採用はまだまだ少ないです。

Performance

パフォーマンスはもう一つ重要な比較ポイントです。それはどのようにAngular 2とReactが重いロード下で処理しているかを示し、アプリケーションが大きくなりすぎて最適化がより難しくなるような将来の問題を予見してくれます。

以下のテストはStefan Krauss (krausest)氏によって開発されてきました。このテストは、ユーザーのアクションをエミュレートし、結果を(ミリ秒で)記録するためにSeleniumと一緒に実行させます。

benchmarks
benchmarks

React(v15.3.0)の方が、Angular 2(RC4)と比べて、同じかより速いということがわかります。Reactは、全ての行のデータを置き換えることになると遅れを取っています(Angular 2: 192.23ms vs React: 214.07ms)。Angular 2はまた要素の選択においても良い結果(3.32ms vs. 6.78ms)が出ています。しかし、Angularは要素の追加においては2倍(611.75ms vs. 293.82ms)遅くなっています。

メモリの管理の面では、ページロード後は3分の1(15.30ms vs. 4.75ms)、DOMに新しい要素を追加する際は2分の1(21.10ms vs 10.67ms)のメモリしか使われないということで、Reactの方がAngular 2よりも優れています。

Reactが明らかな勝者となっていますが、結果は完全に公平に考えられていません。Angular 2はまだ開発段階にあります。開発者の目はスピードの最適化というよりは、バグフィックスや機能の仕上げの方に向いてます。劇的なスピードの改善はやがてくるアップデートやヴァージョンにおいて実現することが予想されます。

Cross-platform integration

クロスプラットフォームの開発の世界はモバイルデバイスの大規模な採用により大きく躍動しています。現在、クロスプラットフォームのモバイルアプリケーションを構築するためのWebビューをベースとしたツールは廃れてきています。

Nativeの機能を持ったクロスプラットフォーム・アプリケーションを構築するAngular 2とReact向けのツールがあります。NativeScriptReact Nativeです。

NativeScriptは、モバイルアプリケーションをAngular 2で開発するのを可能にすることを意図されてTelerikによって開発されました(今も継続中です)。GitHubでのスター数はたったの8,000ですが、NativeScriptは成熟しており、素晴らしいドキュメントと多数のエクステンションが備わっています。NativeScriptは「write once, run everywhere(一度書けば、どこでも実行できる)」というクロスプラットフォームアプリケーションを開発する方法論を説いています。効率的なアプローチとなっていますが、開発者達に多くのプラットフォームに特有のUIの機能を使うことを可能としていません。

一方Facebookによって開発されているReact NativeはReactにネイティブの機能を与えたものです。NativeScriptに比べて、React Nativeはまだ若いですが、かなり人気があり急速に成熟していて、GitHubのスター数はなんと37,000以上となっています。React Nativeはクロスプラットフォームアプリケーションを開発するアプローチとして「learn once, write anywhere(一度学べば、どこでも書ける)」を提唱しています。NativeScriptが行うような異なったプラットフォームを一般化するような方法に代わって、React Nativeはプラットフォームごとの違いを包含し、それぞれのUIを構築するためにそれぞれ異なった方法を提供するようになっています。最近、Angular 2チームはAngular 2のレンダラを作る際にReact Nativeを採用し始めています。

Adoption

Angular 2のGitHubのスター数は恥ずかしながら16,000です。2017年になる前にAngular 2がリリースされることを期待しながらリリースされるまで、どれくらいのissueが残っているかを追っている人たちがいます。Angular 2は相対的に若いですが、そのコミュニティは急速に成熟しています。それは多くのAngular 1の開発者がAngular 2に切り替え始めているからです。すでに「Angular 2」を含み、TypeScriptで書かれたリポジトリがGitHubに6,500ほどあります。

一方ReactのGitHubのスター数は約50,000となっています。「react」というワードを含んだリポジトリ数は76,122となっています。これはAngular 2の10倍以上となります。これだけ多くのリポジトリがある大きな理由のひとつは、機能が最小限に抑えられ、フルスケールのアプリケーションを開発するために必要とされるツールの提供をコミュニティに依存しているからです。

Conclusion

Angular 2もReactも巨大なコミュニティを持ち、背後には企業がついています。例えそれぞれがどのようにアプリケーションを構築するかのアプローチについて異なった哲学を持っていたとしても、ソフトウェア開発の世界の中に両者のための場所がしっかりとできあがったと言えるでしょう。

当記事は、React vs. Angular 2 | hack.guides() by Hristo Georgievを日本語に翻訳したものです。

JavaScript フレームワーク入門 秀和システム
  • 『JavaScript フレームワーク入門』
  • 著者: 掌田津耶乃
  • 出版社: 秀和システム
  • 発売日: 2016年9月16日
  • ISBN: 978-4798047843

コメント一覧

  • 必須

コメント