NHN Cloud NHN Cloud Meetup!

JavaScriptフレームワークの概要4 – React

これから4回にわたり、JavaScript(フロントエンド)フレームワークについて紹介したいと思います。以下のような内容で連載する予定です。

  1. Cycle.js
  2. Angular 2
  3. Vue.js
  4. React

React

はじめに

Reactは、Facebookで開発され、Instagram、Airbnbなどで使用されているオープンソースのUIフレームワークです。

ユーザーアクション基盤でDOMをその都度扱う過去の開発方式(jQueryのようなライブラリのみを使用)とは異なり、開発者がDOMを直接扱うことはなく、Reactがデータの状態に応じて自動でUIを管理するため、開発者は単に特定の状態に対するビューの変化だけを実装すればよくなります。

Reactは次のような特徴を持っています。

  • UIコンポーネントを作成するライブラリで、Reactのコンポーネントはツリー形式で構成されている
  • Virtual DOMを用いて変更部分を最小限のDOM処理でUIを更新し、アプリケーションの性能を向上させる
  • 親コンポーネントからサブコンポーネントに伝達する一方向のデータフローで、容易にデータ追跡とデバッグができる

開発環境

基本実装

var HelloReact = React.createClass({
  render: function() {
    return React.DOM.p(null, 'Hello' + this.props.message);
  }
});

ReactDOM.render(
  React.createElement(HelloReact, {message:'React'}),
  document.getElementById('container')
);

上のコードは、「Hello React」というフレーズを表す簡単なサンプルです。

React.createClass()は、ビューの最小単位であるコンポーネントを作成するAPIで、サンプルのコンポーネントはDOM.p()<p>Hello React</p>APIでをレンダリングするように実装しました。ReactDOM.render()でコンポーネントをDOMに直接レンダリングします。

ReactDOMは、DOMとReactを接続する役割を持つオブジェクトで、コンポーネントをレンダリングしたり、DOMを直接ナビゲートするために使用します。

ReactDOMは、もともとReactに含まれていましたが、react-native、react-canvasのような他のプラットフォームのビューモジュールのような関係に位置づけするように、0.14バージョンから別のモジュールに分離しました。関連記事

HelloReactコンポーネントのリターンエレメントの形態を少し変えてみよう。<p><span>Hello</span><span>React</span></p>のように、HelloとReactを2つのspan要素に区分して表示したい場合は、以下のようにコードを変更する必要があります。

var HelloReact = React.createClass({
  render: function() {
    return React.DOM.p(
      null,
      React.DOM.span(null, 'Hello'),
      React.DOM.span(nul, this.props.message)
    );
  }
});

コードから、どのコンポーネントがどのエレメントを返却するか把握できますか?

JSX

ReactではUI表現の便宜上、XML形式の構文をJavaScriptの構文間で使用できるJSX文法を提供しています。JSXを使えば従来では難しかった重畳エレメント表現もより簡単にできます。

var HelloReact = React.createClass({
  render: function() {
    return <p><span>Hello</span><span>{this.props.message}</span></p>;
  }
});

ReactDOM.render(
  <HelloReact message="React" />,
  document.getElementById('container')
);

このJSX文法を使用したファイルは、一般的に*.jsxという拡張子で保存し、配布前のトランスファイルからjsに変換する過程を通ります。開発環境の設定は後述します。

ES6文法

プラットフォームや開発環境がすでに構築されている場合、React.createClass()よりもES6のclassキーワードを使用することをお勧めします。このclassキーワードは、0.13バージョンからサポートを開始したES7のproperty initializerと一緒に使うと便利です。

// es6 class
class HelloReact extends React.Component {
  // es7 property initializer
  state = {message: 'hello world'};

  constructor() {
    super();
    this.name = 'john';
  }

  // es7 decorator
  @autobind
  greeting() {
    return this.name;
  }

  render () {
    return <p>Hello {this.state.message} {this.greeting()}</p>;
  }
};

classキーワードを使ったところですぐに得られる大きなメリットはありませんが、今後、classキーワードがnative対応になった場合、もう少し速いパフォーマンスがサポートされるでしょう。ES6の様々な便利な文法(spread)を追加的に利用すると、コード量を大幅に削減できます。

classキーワードが今すぐ必要な場合、thisバインディングを直接行う必要があることと、mixinを使用できないという問題がありますが、thisバインディングは、今後追加されるdecoratorで簡単に解決できます。また、mixinはフォーラムでコードの複雑さを増加させるという意見が多く、HOC(High Order Component)に置き換えられる可能性が高いです。慎重に予測すると、React.createClass()は今後消える可能性があります。

開発環境の設定

JSXとES6文法を使用するには、別の変換過程が必要です。そのためにはBabelが必要で、これを使用するには、webpackなどのバンドルツールが必要となります。

Babel、webpackを使用するには、関連するnpmモジュールをすべてインストールする必要があり、複数の設定ファイルを1つ1つ作成しなければなりません。見方によっては非常に面倒ですが、ありがたいことに、Reactではこのような過程を1回で処理してくれるCreate React APPというnpmモジュールを提供しています。モジュールをインストール後、create-react-appコマンド1つで、ローカルサーバーの実行から自動バンドリングまでサポートする開発環境を簡単に構成できます。

$ npm install -g create-react-app
$ create-react-app my-app

npmパッケージを利用して直接開発環境を作成したい場合は、パッケージ管理のページを参照してください。

Reactの構造

Reactアプリケーションの全体的な構造は、コンポーネントのツリー形式です。

props

親コンポーネントは、サブコンポーネントにデータを伝達するためにpropsを使用していますが、データを受信したサブコンポーネントでthis.propsという構文で使用できます。「props」は「Hello React」のサンプルで使用しました。

var HelloReact = React.createClass({
  render: function() {
    return <p>Hello {this.props.message}</p>;
  }
});

ReactDOM.render(
  <HelloReact message="React" />,
  document.getElementById('container')
);

親から変更されたpropsを受け取ると、そのコンポーネントとすべての子コンポーネントを再レンダリングします。

state

コンポーネント自体の状態を管理するのにstateが使用できます。stateのデータを用いて、サブコンポーネントにpropsを転送したりします。stateを変更するには、setState()を使用します。

class LikeButton extends React.Component {
  constructor() {
    super();
    this.state = {
      liked: false
    };
    this.handleClick = this.handleClick.bind(this);
  }
  handleClick() {
    this.setState({liked: !this.state.liked});
  }
  // ...
}

setState()メソッドが呼び出されるときにも、当該コンポーネントとすべての子コンポーネントを再レンダリングします。

コンポーネントのライフサイクル

コンポーネントがページのDOMツリーに実際に追加されるときマウント(mount)され、DOMツリーから削除されるときアンマウント(unmount)されると定義すると、コンポーネントのマウントとアンマウント間には、次のようなライフサイクル(lifecycle)関数が動作します。

  • componentWillMount() – マウント直前に一度発生
  • componentDidMount() – マウント直後に一度発生
  • componentWillReceiveProps() – 新しいpropsが配信される前に発生
  • componentWillUpdate() – props、stateのアップデート直前に発生
  • componentDidUpdate() – props、stateのアップデート直後に発生
  • componentWillUnmount() – アンマウントの直前に発生

ライフサイクル関数が動作するとき、各サイクルに対応するpropsとstate情報を提供し、これに基づいて目的の操作を行うことができます。

コンポーネント間のデータの受け渡し

Reactは一方向のデータフローを持つことから、親コンポーネントからサブコンポーネントにデータ(props)を渡すことができます。

では、上下関係ではないコンポーネント間のデータ転送が必要な場合は、どのように処理すればよいでしょうか?親が同じコンポーネント間であれば、親のstateを利用できます。親コンポーネントが自分のstateを変更できる機能をpropsからサブコンポーネントに付与し、サブコンポーネントは、その関数を使って状態を変更したり、共有することができます。


次に、コンポーネント間の距離が遠い場合は、どのように処理すればよいでしょうか?
propsを再帰的に渡しながら処理すると、どうにかできるようですが…あまり良い方法ではないですね。

このような場合は、データの共有が必要なコンポーネントが共通のモデルを眺めることができれば、解決するのではないでしょうか。Reactのように一方向のデータフローを持っていれば、さらに良いようです。
このような悩みを解決してくれるのが、他ならぬReduxです。

ReduxとReact

ReduxはFacebookで開発された一方向のデータフローの構造を持つFluxの実装体で、JavaScriptアプリケーション向けの予測可能な状態コンテナです。

ReduxはStore、Action creator、Reducerで構成されており、

  1. Storeのデータにビューをレンダリング
  2. ビューで発生したイベントにAction(Action creatorで作成)を作成
  3. Reducerは生成されたActionにStoreを更新
  4. 更新されたStoreにビューレンダリング

といった一方向の流れを持ちます。

単一方向ならReactと同じではないでしょうか?両方を一緒に使ってみましょう。ReactをReduxのように使用する場合は、下図のようなフローになります。
出典:Getting Started with React, Redux and Immutable: a Test-Driven Tutorial (Part 2)

図の上では完璧ですね。ところがReduxを使っても、最上段のコンポーネントにのみRedux storeが接続されると、以前と同じではないでしょうか。前章のような距離が遠いコンポーネント間のデータ転送の問題は解決されません。

出典:https://css-tricks.com/learning-react-redux/

幸運にも、コンポーネントの深さと関係なく、Redux storeにアクセスできる方法が提供されています。Reduxはreact-reduxというnpmモジュールを用いてReactで使用できますが、react-reduxモジュールのconnectというメソッドを使用すると、ReactコンポーネントでRedux storeにアクセスできるスマートコンポーネントを作成できます。このときconnectメソッドの引数としてmapStateToProps mapDispatchToPropsの2つの引数を渡します。それぞれの役割は以下の通りです。

  • mapStateToProps:図の青い矢印の役割をする関数。コンポーネントに必要な値をstoreから直接照会する役割を持つ。
  • mapDispatchToProps:図で緑色の矢印の役割をする関数。ユーザーのアクションで発生するstoreの変化を実装する。

Reduxなしで親コンポーネントからpropsのみを受け取るコンポーネントをdumbコンポーネントと呼びます。

このスマートコンポーネントを使うと、コンポーネント間のデータ転送問題を解決できます。データ配信が必要なすべてのコンポーネントをスマートコンポーネントとして作成すると、コンポーネント間でRedux storeを通じてデータを共有するため、問題が解決されます。

import {connect} from 'react-redux';

// DUMB COMPONENT
class HelloWorld extends React.Component {
/* ... */
}

// Storeでコンポーネントに必要な値をpropsで照会する。
const mapStateToProps = state => ({
  name: state.name
});

// `bindActionCreators()`で単純なaction creator関数をstoreと直接連結する。
const mapDispatchToProps = dispatch => bindActionCreators({
  name: function(state, action) {
    const {type, newName} = action;

    switch (type) {
      case 'CHANGE_NAME':
        return newName;
      default:
        return state;
    }
  }
}, dispatch);

// SMART COMPONENT
const Container = connect(mapStateToProps, mapDispatchToProps)(HelloWorld);

イベント処理

ReactDOMでイベントバインディング

ReactDOMでDOMに割り当てることができる属性を定義するときは、JSX要素に<button class=”btn” />のようにプロパティとして定義しますが、イベントハンドラも同様にJSX要素にcamel caseで(<button onClick=’…’ />)を割り当てます。

function onClick(e) {
  //...
}

class Btn extends Component {
  render() {
    return (
      <button onClick={e => onClick(e)} />
    );
  }
}

イベント委任(delegation)

JSX要素にイベントを割り当てると、Reactはイベントを当該要素にバインドせず、document.bodyに委任した形で動作させます。イベント委任の利点は、こちらを参照してください。

統合的な(Synthetic)イベント

Reactの割り当てられたイベントハンドラには、ブラウザのネイティブイベントのクロスブラウザのwrapperであるSyntheticEventのインスタンスが配信されます。SyntheticEventはネイティブイベントのようなインタフェースを保有しています。ネイティブイベントが必要な場合、.nativeEventを使用することもできます。

SyntheticEventはプール(pooling、イベントオブジェクトを再利用するためにイベントオブジェクトを管理)されるので、イベントハンドラ内で非同期でイベントオブジェクトにアクセスするとnullを返すようになります。

function onClick(event) {
  console.log(event); // => nullified object.
  console.log(event.type); // => "click"
  var eventType = event.type; // => "click"

  setTimeout(function() {
    console.log(event.type); // => null
    console.log(eventType); // => "click"
  }, 0);

  // Won't work. this.state.clickEvent will only contain null values.
  this.setState({clickEvent: event});

  // You can still export event properties.
  this.setState({eventType: event.type});
}

非同期でイベントオブジェクトにアクセスするには、サンプルのように変数にキャッシュ(var eventType = event.type)した後、使用します。

テスト

テストツール

一般的にWebアプリケーションのテストは、レンダリング以降のマークアップを確認する形で実行します。この概念さえ分かれば別のツールがなくてもテストできます。まず、HTMLページとテストコードを用意しましょう。

<!-- 1. HTML文書の準備 -->
<script src="service.js"></script>
<script>
  // 2. テスト実行
  shouldRenderListProperly();

  function shouldRenderListProperly() {
    // 3. root要素の準備
    var root = document.createElement('div');
    root.setAttribute('id', 'root');
    document.body.appendChild(root);

    // 4. mockデータ準備
    var mockList = ['hello', 'world'];

    // 5. コンポーネントレンダリング
    ReactDOM.render(<MyComponent list={mockList} />,
    document.query`Selector`('#root'));

    // 6. 結果確認
    console.assert(document.query`Selector`('ul li').length === 2);
  }
</script>

コンポーネントがレンダリングされるコンテナ要素を作成して、レンダリング後のマークアップを調べる2〜6の手順を繰り返すことで、テストカバレッジを向上させることができます。しかし、この方法は生産性が非常に低下します。このとき使用するツールが、KarmaJasmine enzymeです。

Karmaは、Webサーバーを内蔵する実行プログラムです。[設定ファイル]を読んで、必要なソースが含まれるHTMLファイルを作成したり、ブラウザで開いて特定のJS関数を自動実行する機能を持っています。また6の結果項目を、希望する形状(console、junit、teamcity)のフォーマットで出力することも可能です。(CIに応用できます)

Jasmineはdescribe()it()メソッドを提供するテストツールです。3〜5のコードがテストに含まれている場合、テストの意図が希釈され、beforeEach()beforeAll()などのAPIを使うと、重複したコードを削除してすっきりとしたテストを作成できます。

コンポーネントのマークアップが複雑になるほどテストコードも複雑になりがちですが、enzymeはReactコンポーネントのテストツールにおいて、抽象化APIからすっきりとしたマークアップを確認できるAPIを提供します。

説明したツールを使ったテストコードです。

import {mount} from 'enzyme'

describe('component', () => {
  describe('MyComponent', () => {
    let component, props;

    beforeEach(() => {
      props = {
        mockList = ['hello', 'world'];
      };

      component = mount(<MyComponent {...props} />);
    });

    it('should render list properly.', () => {
      expect(component.find('li').length).toBe(2);
    });

    it('...');
    it('...');
  });
});

Shallow Rendering

上のサンプルで、リストの項目の1つ1つが特定のpropsを要求するReactコンポーネントであると考えてみよう(仮称ListItemコンポーネント)。エラーなしでレンダリングするには、MyComponentをレンダリングするとき、ListItemに必要な値までmockingしなければならない煩わしさがあります。

it('...', () => {
  // このmockListをつなぎit構文で準備するのは面倒で、テストの意図を薄める。
  const mockList = [
    {id: 1, name: 'a.txt', size: '900'},
    {id: 2, name: 'b.txt', size: '1222'},
    ...
  ];
});

MyComponentの機能だけをテストしたいのに、ListItemまでテストをすることになる状況になっています。このとき便利な機能がShallow Renderingです。

Shallow Renderingされたコンポーネントは、子コンポーネントを持っていますが、実際にはレンダリングをしていません。したがって、子コンポーネントのpropsをmockingする必要がありません。MyComponentをレンダリングした結果を見ると、次のように出力されます。

const mockList = ['a', 'b'];

// shallow rendering
const component = shallow(<Component mockList={mockList} />);

console.log(component.debug());

// 出力:
//<ul id="comp">
//  <ListItem />
//  <ListItem />
//</ul>

component.find(‘ListItem’).lengthをチェックすることで、リスト数に合わせてレンダリングするかテストできます。v15.0.0を基準に一般的なReactコンポーネントは、特別な設定をせずにレンダリングすることができ、SFC(Stateless Functional Component)の場合、displayNameの値を書けば、コンポーネントの名前で検索できるようになります。

パフォーマンス – レンダリング

Reactアプリケーションは、複数のコンポーネントがツリー形式で構成されています。特定コンポーネントの状態が変更されたとき、サブコンポーネントが連鎖的にレンダリングされ、運営される構造です。

Reactは、基本的に実際に変更された部分だけを検出して、DOMを操作するように最適化されたレンダリングエンジンを持っています。公式サイトにあるO(n3)の複雑さをO(n)で減らす方法は、かなり興味深い記事ですが、基本的なガイド文書に従えば、自然とこのアルゴリズムが適用され、サービスとして問題ない性能が保証されます。

むしろReact公式文書では、中途半端な最適化は、デバッグを困難にする恐れがあるため、必要なところでのみ最適化を適用するようにガイドしています。

では、本当に必要な場所はどこにあるのでしょうか?

Reactアプリケーションの全体構造はコンポーネントツリーであると先述しました。特定の親コンポーネントの状態が変更された場合、すべてのサブコンポーネントがレンダリングされますが、このとき一部のコンポーネントは、レンダリングしなくてもよいか(A)、ある条件でのみレンダリングすればよいか(B)、いずれかのケースになります。この部分がまさに最適化のポイントです。

最も簡単な例として、(A)はマークアップのみを含むコンポーネントが挙げられます。もちろんオーバーヘッドは大きくありませんが、アプリの性能に問題がある場合、同様のコンポーネントに適用すると有用でしょう。

反復的なリストをレンダリングする場合、(B)の最適化は大いに助けられます。例えば、タスクの内容、締切、ステータスを管理できるTODOリストがあるとしましょう。3つの状態のうち、ビューに表示されるのは、タスクの内容とステータスだけです。締切の変更は、Webページに表示されなくてもよいですね。それなら、タスクの内容、ステータスの2つの状態の変更のみDOM操作でつながればよいでしょう。その条件をshouldComponentUpdateに実装すると、リストの量に比例して膨大な性能向上を実現できます。

優れた設計者は早急にコードの最適化を行いません。Reactのツリーレンダリングのパフォーマンス最適化方式は、この言葉通り計画の最適化を可能にします。

パフォーマンス – Selector

ReduxのAPIのうち、ユーザーが直接実装するmapStateToProps関数は、Storeからコンポーネントに必要な特定の値を選択するために使用します。この役割からSelectorとも呼ばれますが、このSelectorStoreが変更される度に実行され、コンポーネントに新しい状態を通知します。しかし、パラメータが同じときも値を計算するオーバーヘッドがあります。

Selectorは、通常、パラメータが同じであれば、戻り値も同じ純粋関数である特性があります。したがって、前述したオーバーヘッドを減らす方法として、メモ化パターンを適用できますが、Reselectは簡単にこの機能を実装できるAPIを提供します。また、作成されたSelector同士を組み合わせる機能も提供しており、はるかに柔軟な設計ができます。

Storeで次のコードのようなReactアプリケーションがあると仮定します。

/* STORE */
{
  name: 'john',
  age: 29,
  friends: ['albert', 'kim']
}

/* COMPONENT */
class HelloWorld extends Component {
  render() {
    return <h1>{this.props.greetings}</h1>;
  }
}

function getGreetings({name, age}) {
  // 費用が必要な計算過程
  return `Hello i'm ${name} and ${age}`;
}

const mapStateToProps = state => {
  return {greetings: getGreetings(state)}
};

getGreetingsのテンプレート演算はnameageが変更された場合にのみ実行されます。しかし現状では、friendsのみが変更されても実行されます。Reselectを使ってリファクタリングしてみよう。

const getName = state => state.name;
const getAge = state => state.age;

const getGreetings = createSelector(
  // Reselectで下の関数らをInput'Selector'と呼ぶ。
  [getName, getAge],

  // Reselectで下の関数は、Result 'Selector'と呼ぶ
  (name, age) => {
    return `Hello i'm ${state.name} and ${state.age}`;
  }
);

const mapStateToProps = state => ({
  greetings: getGreetings(state);
});

getGreetingsは、ReselectのAPI createSelector()によって最適化されています。パラメータが同じであれば、テンプレートの演算をせずに、キャッシュされた以前の実行結果を返します。friendsのみ変更された場合、getGreetingsのテンプレート演算は行われません。

ガイド文書によると、パラメータの比較は、===演算子で実行し、APIを通じていくらでも変更できます。もし、getGreetingsがプロジェクト全体でユーティリティ的な性質として使用されるなら、別途モジュールを作成して、プロジェクト全体のパフォーマンスを向上させることができます。

その他 – Redux-Saga

ここでは必須ではありませんが、状況に応じて便利に使用できるツールを紹介します。
まれにガイドで推奨されている方法よりも、効率的だと考える方法で実装することがありますが、このとき解決しにくい問題が生じる可能性があります。

Reactに掲示板アプリケーションを作るときStoreの構造を、次のような2つで設計します。

データを積みビューに表示される記事はidで参照

{
  currentContentId: 'id1',
  contents: {
    id1: '掲示板の本文...',
    id2: '掲示板の本文...',
  }
}

本文をクライアントに積み、ビューに表示される本文はidを使用する方法です。スレッドを見れば見るほど、データがたまるという短所があり、あまり使用されていません。

プロパティを直接使用

{
  content: '掲示板の本文...'
}

内容を即時にビューに露出させる形態です。一般的に最小限の情報を保持する設計が好まれるため、この方法で実装します。しかし、contentを変更させるactionが非同期にdispatchされた場合、問題が発生します。

ユーザーがすぐに実行できるアクションのAPI呼び出しを実装する場合は、応答の順序で望ましくない結果が発生することがあります。例えば、SPA(Single Page Application)形態のアプリケーションで一貫性のあるユーザー体験のため、ブラウザの前後機能を実装する場合があります。

contentが実際に変更される時点はAPIレスポンスが到着した時点、すなわち非同期時点です。ブラウザの前、後、前のショートカットを使って素早く移動した場合、API要求順が分からなくても、応答(前(1)、後(2)、前(3))の順序は、ネットワーク環境のようないくつかの要因によって保証されません。結局、ユーザーはブラウザのURLと全く関連性のないページを見ることになります。

これを解決するには、一部のactionのdispatch順に同時実行制御が必要です。このとき、Redux-Sagaツールが大いに役立ちます。実際にSagaパターンは、トランザクション処理において原子性と可用性をトレードオフで、一連のサブトランザクションを完全に成功させ、または中途障害時の完全復旧を保証します。

Redux-Sagaはこのパターンでアイデアを採用し、一連のaction dispatchingが完全に終了するか、中途障害時は1つもdispatchingしないこと(Storeが変わらないこと)を保証する機能を提供しています。この機能を利用すると、前述した要求と応答の手順に沿った問題を解決できます。

import {takeLatest} from 'redux-saga';
import {call, put} from 'redux-saga/effects';

/**
 * 関数を直接実行せずにcall,putのようなredux-sagaのAPI利用して
 * 同時性機能を利用する。
 */
function* routeToFileList() {
  try {
    // ローディングイメージ出力のためのアクション
    yield put({type: 'FETCH_FILES_REQUEST'});

    // API呼び出し
    const {data} = yield call(axios.get, url, params);

    // データを取得したことを知らせるアクション
    yield put({type: 'FETCH_FILES_SUCCESS', data});
  } finally {
    // ファイルリストを受け取るジョブの終了を知らせるアクション
    yield put({type: 'FETCH_FILDS_DONE'});
  }
}

/**
 * redux-sagaはアプリケーションにmain saga 1つを支援する。
 * Generatorの特性によって、このmainは組み込みプログラミングの
 * void main() のように終了せずに継続して実行される。
 */
function* main() {
  /**
    * takeLatestは最後に始まったsagaにのみdispatchingを保証する。
    */
  yield takeLatest(['ROUTE_TO_FILE'], routeToFileList);
}

このツールは、現在も非常に活発にメンテナンスされています。また、質問的な問題も親切に回答してくれます。

takeLatest() について質問した問題には、絵で説明してくれました。

takeLatest() だけでなく、takeEvery()throttle()などの高度な同時実行制御機能とプロセスブロックなしでsagaを実行するforkは、saga間のデータ通信用channelなどのAPIを提供します。同時実行制御の高度関数だけでも十分に価値のあるツールです。

その他 – Normalizr

Storeは可能な限り薄く設計した方がよいでしょう。下記のように、authorというモデルで、depthがこのような場合、articles Reducerはauthorの修正に3-depthの値を変更するため、複雑な実装をするしかありません。

/* STORE */
{
  articles: [{
    id: 1,
    title: 'Some Article',
    author: {
      id: 1,
      name: 'Dan'
    }
  }]
}

/* COMPONENT */
const articles = (state, action) {
  const {type} = action;

  switch (type) {
    case 'UPDATE_AUTHOR':
      const {id, newName} = action;
      // すべてのarticleを巡回しながら、authorを探す必要がある
      for (const article of state) {
        if (article.author.id === id) {
          article.author.name = newName;
          return {...state};
        }
      }
      return state;
    default:
      return state;
  }
}

しかし、以下のようにStore最適化すると、Reducerを簡単に実装することができます。

/* STORE */
{
  articles: {
    '1': {
      id: 1,
      title: 'Some Article',
      author: 1
    }
  },
  authors: {
    '1': {
      id: 1,
      name: 'Dan'
    }
  }
}

/* COMPONENT */
const articles = (state, action) => {
  /* articles値だけを気にすればよい。 */
};

const author = (state, action) => {
  const {type} = action;

  switch (type) {
    case 'UPDATE_AUTHOR':
      const {id, newName} = action;
      if (state[id]) {
        return {...state, [id]: {name: newName}};
      }
    default:
      return state;
  }
};

したがって、可能であればドメインモデルを定義して、Storeが深くならないように管理することが重要です。ところが、まれに外部APIを連動する場合、対象サイトのポリシーに応じて、データの型が複雑になる場合があります。

// API応答結果
{
  id: 1,
  title: 'Some Article',
  author: {
    id: 7,
    name: 'Dan'
  },
  contributors: [{
    id: 10,
    name: 'Abe'
  }, {
    id: 15,
    name: 'Fred'
  }]
}

この場合、前述した理想的なStore設計にデータを反映するには、前処理を別にしなければなりませんが。Normalizrはスキーマ定義を使って、これを簡単に正規化できる機能を提供してくれます。サンプルコードを実行すると関数が作成され、この関数にJSON応答データをパラメータとして実行すると、すぐに正規化されたデータを受信できます。

const article = new Schema('articles');
const user = new Schema('users');

article.define({
  author: user,
  contributors: arrayOf(user)
});

const result = normalize(response, article);

console.log(result);

// {
//   result: 1,                    // <--- Note object is referenced by ID
//   entities: {
//     articles: {
//       1: {
//         author: 7,              // <--- Same happens for references to
//         contributors: [10, 15]  // <--- other entities in the schema
//         ...}
//     },
//     users: {
//       7: { ... },
//       10: { ... },
//       15: { ... }
//     }
//   }
// }

正規化演算のオーバーヘッドをトレードオフに、Reducerで扱いやすいデータを作成できます。

結論

これまでReactの基本内容からパフォーマンスの最適化まで取り上げました。Reactは、Webアプリケーション開発の最大の障害であるパフォーマンスの問題をVirtual DOMを通じて簡単に解決できるソリューションを提供しています。特に、JSX文法を使って既存のHTMLマークアップを作成するように、コンポーネントが作成できるという点は大きな利点でしょう。

似たようなWebアプリケーションフレームワークの中でもユーザー層が最も厚くなっています。複雑で難しいことを簡単に解くことができる数多くのツールが存在しますが、よく選択して使用すれば、専門知識がなくても高品質のアプリケーションを開発できるでしょう。また「その他」の項目で紹介したツールは、すべてReactの開発者、Dan Abramovが寄与して、推奨しているものでもあります。

NHN Cloud Meetup 編集部

NHN Cloudの技術ナレッジやお得なイベント情報を発信していきます
pagetop