俺、サービス売って家買うんだ

Swift, Kotlin, Vue.js, 統計, GCP / このペースで作ってればいつか2-3億で売れるのがポっと出来るんじゃなかろうか

React + Reduxでマウスについてくる星を作る

f:id:ie-kau:20160707123950p:plain


さて、皆様七夕の夜をいかがお過ごしでしょうか?
七夕ということで、マウスの軌跡を星が追従するJavaScriptを書きました。

作ったもの

www.youtube.com

DEMO ※一旦PCのみ

もうホント簡単なんですが、Web1.0時代に流行していたマウスに画像を追従させるあれです。あれ。JavaScriptと言ったらこれですよね。最高!


本当にやりたっかたこと

  • React + Reduxの勉強
    • 設計 - 実装 - テストまで一通りの流れの経験

ここ一年ぐいらフロントエンドを書くときは「慣れてるから」という理由でVue.jsを利用していたのですが、最近はReact + Reduxで実装しているサービスが増えており、そろそろ勉強しないとという焦燥感がでてきて何かいいアイディアはないかと考えていた折に、某同僚と

React + Reduxを使ってマウスについてくる星実装してようぜ

となった勢いで実装してみました。
それで、実際に作ってみた結果、新たに学んだこと・ハマったこと・課題辺りをつらつらとまとめておきます。

Framework / ツール

Framework React + Redux
Test Mocha + Expect
Linter ESlint

Linterのルールはairbnbのものを利用しています。

設計

  • React + Reduxで書かれたアプリケーションのRootとなるCompoentをHigher-Order Components(※後述)に入力するとマウスに追従する星を付与したCompoentが返される
  • 星の機能は流行が廃れる可能性があるので簡単に外せるようにする

実開発において、開発がかなり進んだタイミングで「マウスの軌跡を追従する星を表示したい」という要件が割り込んできた時に耐えられる作りにすることを念頭においています。

ソースコード

github.com

ディレクトリ構造

├── actions
│   ├── actionCreators.js
│   └── actionTypes.js
├── components
│   ├── app.js
│   ├── atoms
│   │   └── star.js
│   └── molecules
│       └── stars.js
├── containers
│   └── app.js
├── index.js
├── reducers
│   └── index.js
└── store
    └── configureStore.js

components/app.jsがrootのComponentになっていて、containers/app.js内にComponentをラップする関数が記述されています。

学んだこと

Rect + Reduxの使い方以外に学んだ新しい事項です。

1. Atomic Design (Atomic Components)

化学の原子/分子/有機体までをアナロジーとして、下記5分類にコンポーネントの粒度を分けたデザインの方法論のことだそうです。

  1. Atoms
  2. Molecules
  3. Organisms
  4. Templates
  5. Pages

詳しくはこの辺を読むとわかります。

今回の実装では一つのStarをAtomとして、星が連なっているStarsをMoleculeとしてみました。頻出のパターンだとリストの一行が Atom でまとまったULが Moleculeといったところでしょうか。
ただReactのComponents設計を主眼において言葉を知ったためにTemplatesとPagesの分類が曖昧でした。そこをデザインの方法論として理解すると以下の通りだそうです。

Templates

  • 化学のアナロジーから離れて、最終的なアウトプットとして意味を成すもの
  • 複数のOrganismsの集合からなる
  • デザインをみてレイアウトを検討し始めるの段階
    • 抽象的なMoleculesやOrganismsの関係を提供する

言ってみれば、ワイヤーフレーム。デザインワークフロー上ではレイアウトや情報設計を行うタイミングの中間成果物。

Pages

  • Templatesでレイアウトだけだったものにコンテンツを入れた実成果物

参考記事でinstanceと表現されていたとおり実際にページです。

今回はHello worldを出力しただけで複数ページを作ったわけではなかったので、TemplatesやPagesというディレクトリや各ページ向けの.jsファイルの命名規則を検討する必要はありませんでしたが、TemplatesとPagesはReactのファイル構成上一緒くたにしても良さそうでした。

2. Higher-Order Components

先に定義の話

A higher-order component is just a function that takes an existing component and returns another component that wraps it.

既存のComponentを受けて、それをラップして別のCompoenentにして返す関数です。その名通り高階関数のComponents版。 今回の星の件でいうと、ここ。

function star(Component) {
  class StarryComponent extends React.Component {
    static get FPS() {
      return 30;
    }

    componentDidMount() {
      this.tick();
      document.addEventListener('mousemove', this.props.handleMousemove);
    }

    componentWillUnmount() {
      this.props.handleDidStarsUnmounted();
      document.removeEventListener('mousemove', this.props.handleMousemove);
    }

    tick() {
      if (this.props.star.isEnabled) {
        this.props.handleUpdate();
        setTimeout(() => { this.tick(); }, 1000 / StarryComponent.FPS);
      } else {
        this.props.handleInitializeStar();
      }
    }

    render() {
      return (
        <Stars starCoords={this.props.star.starCoords}>
          <Component />
        </Stars>
      );
    }
  }

  StarryComponent.propTypes = {
    star: PropTypes.object.isRequired,
    handleDidStarsUnmounted: PropTypes.func.isRequired,
    handleInitializeStar: PropTypes.func.isRequired,
    handleMousemove: PropTypes.func.isRequired,
    handleUpdate: PropTypes.func.isRequired,
  };

  return StarryComponent;
}

https://github.com/hazumu/redux-stars/blob/master/src/containers/app.js#L20

なんでこんなことをするか?

  • そもそもes6のclassにmixinが使えない

No Mixins Unfortunately ES6 launched without any mixin support. Therefore, there is no support for mixins when you use React with ES6 classes. Instead, we're working on making it easier to support such use cases without resorting to mixing.

とりわけルールを縛って純粋な関数としてComponentsを記述するとlifecycle methodsをComponents側に定義できなくなってしまうので、lifecycle methodsをHigher-Order Componentsの方に実装してComponentsをlifecycle methodsに依存しないより汎用的なものとして書くことができます。

※参考 medium.com

ハマりどころ

1. Component should be written as a pure function

ハマったというか知らなかったのですが、ESlintの設定をデフォルトのままにしていたため以下のワーニングに悩まされました。

error  Component should be written as a pure function   react/prefer-stateless-function

Componentsをステートレスにするため純粋な関数にすることがReactの推奨なのかと思い、ちまちま直したのですがReactのLinterが推奨しているだけで特に推奨ってわけではなさそうでした。
公式ドキュメントのこの項目で使えるとは言ってるけど、絶対やれとは別に言われていません。
Reusable Components | React

また、次期バージョンのReactではReact.PureComponentという状態をもたせずinputに対してHTMLを出力するだけの純粋なComponentsが搭載されるようです。

Add React.PureComponent, inherit purity for functional components by spicyj · Pull Request #6914 · facebook/react · GitHub

2. componentDidMountのタイミグでアクションを発行してもステートが変わらない

どうやらlifecycle methodのcomponentDidMount内でアクションを発行して、stateを変えてアクションを発行した直後に使うメソッドで変更したstateを利用しようとしてもそのタイミングではまだステートが変わっていないようでした。
具体的には下記の流れでcomponentDidMountのタイミングでisEnabledというフラグをtrueにして星の再生に利用しているtickを開始させようと目論んでいました。

  1. componentDidMountが呼ばれる
  2. state内のisEnabledをtrueにするアクションをディスパッチする
  3. tickの開始
  4. componentWillUnmountでisEnabledをfalseにするアクションをディスパッチしてtick終了

しかし、実際には2でstateが変わらずにtickが始まらないという現象にハマってしまいこんなコードになってます。
※深追いはしてないけど、Reactがbatchupdateとかしてるあたりが怪しいかな。。

今後の学習課題

今回の学習では以下事項が学べなかったので続けて勉強していきたいところです。

  • Ajaxなど非同期の処理が学べないのでreact + reduxでいうミドルウェアの層の扱い
  • 複数ページとSSRを利用したパーマネントリンクの実装

まとめ

  • React + Reduxの一通り概念をつかむのにはいい課題だった(諸先輩方にいろいろ教われた)
  • 巷で言われている通りアニメーションの実装が入るとvirtual domだと厄介そうな印象がある
  • 関数を書いていアクションを発火させるだけでUIが変わるのが心地よい
  • 作ったものが小さすぎた

次はAjaxで天気API叩いて、星を晴れマークや曇りマークにしてみるかな〜。•̀.̫•́✧オラッオラッ