hisasann/electron-react-redux-boilerplate

はじめに

Node.js + Babel 6 + React + Redux + Webpack + Gulp + Electron

Electronを母体として、React・Reduxを使ったフロントエンドのアーキテクチャについて試したことをメモメモ

Webpackはbrowserifyとしか使っていません

Electron

最近はやりのElectron(MacやWindowsのアプリ化)を使った場合の開発の仕方や、どうやるのが効率的かを少しずつメモしていきます

現時点では、Electronでビルドしたアプリのリリースやアップデートに関しては未調査になります

Livereload

まずは Livereload これがないとはじまりません

以下の記事を参考に、gulpのタスクを作ってみました

ぼくのかんがえたさいきょうのElectron - Qiita

  1. RendererProcessで読み込んでいるコードが変更されたら, 画面をreload.
  2. BrowserProcess(MainProcess, main.jsなど)で動作するコードが変更されたら, RendererProcessもろともBrowserProcessを再起動

使ったnpmモジュールは、Quramy/electron-connectです。

gulpのタスク

import gulp from 'gulp';

import gulpLoadPlugins from 'gulp-load-plugins';
const $ = gulpLoadPlugins();

import configs from '../config';
const config = configs.electron;

import electronConnect from 'electron-connect';
const electron = electronConnect.server.create();

gulp.task('serve', function () {
  // Electronの起動
  electron.start();

  // BrowserProcess(MainProcess)が読み込むリソースが変更されたら, Electron自体を再起動
  gulp.watch(config.browserProcess, function() {
    console.log('BrowserProcess reload');
    electron.restart();
  });

  // RendererProcessが読み込むリソースが変更されたら, RendererProcessにreloadさせる
  gulp.watch(config.rendererProcess, function() {
    console.log('RendererProcess reload');
    electron.reload();
  });
});

htmlファイル

<!-- gulp側で立てたserverへ接続する -->
<script>require('electron-connect').client.create()</script>

このhtmlは本番環境では不要なので、gulp-userefなどで消去するとよい

React / Flux

via React.js architecture - Flux VS Reflux

ここでは、Fluxについてはふれません

詳しくは、facebook/flux などをお読みください

React / Redux

今回の目玉です

WordPress.comRedux を使って全面リニューアルしたようなので、大規模で使われた良い例なのではないでしょうか

しかも、OSSです

Automattic/wp-calypso

まずはこの図を見て、なんとなく処理がどうながれるか確認しておきましょう

[Read Me Redux](http://redux.js.org/index.html)

また、このリポジトリのサンプルコードは、routingを組み込んだ状態になります

なので、routingを組み込まない場合のReduxの書き方は少し違いますが、以下の内容と合わせて読んでいけば理解できるかと思います

ざっくりと、ReactとReduxを接続する手順としては、

  1. rootのcomponentを **** でwrapする
  2. wrapしたcomponentをrackt/react-reduxのconnectメソッドでReactとReduxをつなぐ
  3. connect()することで、actionがpropに、storeがpropとして受け取れるようになる

では、順番に見ていきましょう

Entry Point

Entry Pointはまさに一番rootに位置するコードで、最も親の部分になります

以下のサンプルコードたちは、シンプルで理解しやすい、

redux/examples/counter at master · rackt/redux を引用しています

import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import App from './containers/App'
import configureStore from './store/configureStore'

const store = configureStore()

render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

ここで大切なのが Provider です

  1. rootのcomponentとしてReactコンポーネントをwrapします

  2. propsとしてstoreを受け取りReduxのオブジェクトに登録する

Container Components

  1. 渡されたstoreはconnect()でwrapされたcomponentを通じて共有される
  2. connectで以下のようにcomponentに接続することでcomponentのpropsにstateとactionが渡される
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import Counter from '../components/Counter'
import * as CounterActions from '../actions/counter'

function mapStateToProps(state) {
  return {
    counter: state.counter
  }
}

function mapDispatchToProps(dispatch) {
  return bindActionCreators(CounterActions, dispatch)
}

export default connect(mapStateToProps, mapDispatchToProps)(Counter)

Action Creators and Constants

actionでは、

bindActionCreators(CounterActions, dispatch)

でactionをbindすることで、この後、reducerに値が流れるようになります

また、Ajaxなどの非同期処理はここでやるのがよさそうです

非同期処理は、Reduxそのままだけではうまく動かないので、下のほうで説明している redux-thunk を使う必要があります

actionでは、何をしたいかをreducerに伝えるのみにし、アプリケーションの状態を変えるようなことはしないほうが設計的によさそうです

export const INCREMENT_COUNTER = 'INCREMENT_COUNTER'
export const DECREMENT_COUNTER = 'DECREMENT_COUNTER'

export function increment() {
  return {
    type: INCREMENT_COUNTER
  }
}

export function decrement() {
  return {
    type: DECREMENT_COUNTER
  }
}


// action creaters
export function incrementIfOdd() {
  return (dispatch, getState) => {
    const { counter } = getState()

    if (counter % 2 === 0) {
      return
    }

    dispatch(increment())
  }
}

export function incrementAsync(delay = 1000) {
  return dispatch => {
    setTimeout(() => {
      dispatch(increment())
    }, delay)
  }
}

Reducers

reducerでは、前のstateとactionより次のstateを計算してstoreに渡します

ここは、ただそれだけに徹する必要があります、そうでないと、設計がグチャグチャになってしまいます

import { INCREMENT_COUNTER, DECREMENT_COUNTER } from '../actions/counter'

export default function counter(state = 0, action) {
  switch (action.type) {
    case INCREMENT_COUNTER:
      return state + 1
    case DECREMENT_COUNTER:
      return state - 1
    default:
      return state
  }
}

reduxではstateをreduxオブジェクトが内部的に管理するため、一般的なモデルのようにgetter / setterを用意したりはしません

また、reducerは、アプリケーションの規模に応じて、細かく分割もできます

つまり、画面ごとに使うreducerを選んで、使うこともできます

その場合は combineReducers を使って、使いたいreducerを選びます

combineReducersに渡したreducerが、actionでgetState()できるstateになるようです

なので、渡す名前をミスすると、アプリケーションがうまく動きません

import { routerStateReducer as router } from 'redux-router'
import { combineReducers } from 'redux'
import counter from './counter'
import errorMessage from './errorMessage'

const rootReducer = combineReducers({
  counter,
  errorMessage,
  router
})

export default rootReducer

JavaScript - Reduxにおけるreducer分割とcombineReducersについて - Qiita

Presentational Components

いわゆるパーツごとのcomponent部分です

たとえば、headerやfooterなど、親のcomponentが使いやすいような単位に分割する部分です

import React, { Component, PropTypes } from 'react'

class Counter extends Component {
  render() {
    const { increment, incrementIfOdd, incrementAsync, decrement, counter } = this.props
    return (
      <p>
        Clicked: {counter} times
        {' '}
        <button onClick={increment}>+</button>
        {' '}
        <button onClick={decrement}>-</button>
        {' '}
        <button onClick={incrementIfOdd}>Increment if odd</button>
        {' '}
        <button onClick={() => incrementAsync()}>Increment async</button>
      </p>
    )
  }
}

Counter.propTypes = {
  increment: PropTypes.func.isRequired,
  incrementIfOdd: PropTypes.func.isRequired,
  incrementAsync: PropTypes.func.isRequired,
  decrement: PropTypes.func.isRequired,
  counter: PropTypes.number.isRequired
}

export default Counter

Smart ComponentsとDumb Components

このリポジトリのコードでもそうですが、 containerscomponents が分かれているところについて少し書きます

これは、Reactにかぎらずですが、この手のアーキテクチャを組む場合、2層構造のcomponentを使うことが多いです

それは親componentと子componentという感じです

以下の記事では、それをSmart ComponentsとDumb Componentsと呼んでいてなるほどなーと思いました

Reduxでは containerSmart ComponentscomponentsDumb Components になります

Reduxとの依存を切り離す Smart ComponentsはReduxとの依存関係を持つ Dumb ComponentsはSmart Componentsからprops経由で全てを受け取るようにする

Reduxの設計で気をつけるところ - なっく日報

この設計思想は、これからもとても大切な考え方なので、きちっと実装していきたいですね

connect()

connectメソッドを使って、共通のpropsを持つ親コンポーネントを作れる

connectメソッドを使ってラップされたコンポーネントはdispatch, props, state を受け取ることができるようになる

ラップしたコンポーネントが対象、子孫コンポーネントには共有されない

計画なくどの階層のコンポーネントでもラップしてしまうと 各コンポーネントのコミュニケーションの流れが壊れるのでラップしすぎないように注意が必要

fluxフレームワークreduxについてドキュメントを読んだメモ - fukajun - 僕はvimで行きます -

Reduxのグローバルな状態とコンポーネントのプロパティをマップする関数と、コンポーネントのプロパティをReduxのActionにマップする関数がある

redux#bindActionCreators()はActionを返すメソッド群をそれぞれdispatch()でラップし、さらに1つのオブジェクトにまとめあげるユーティリティ

reduxを試してみた(2日目) - React.jsを学ぶ - Qiita

bindActionCretors()

Actionをpropsとして、バインドする

[bindActionCreators Redux](http://redux.js.org/docs/api/bindActionCreators.html)

非同期でActionを呼び出す場合

export function showErrorMessageDelayed(message, delay = 1000) {
  return dispatch => {
    setTimeout(() => {
      dispatch(showErrorMessage(message));
    }, delay);
  };
}

middlewareとしてgaearon/redux-thunkを追加します

import thunk from 'redux-thunk';

const finalCreateStore = compose(
  applyMiddleware(thunk),
  reduxReactRouter({ routes, createHistory }),
  applyMiddleware(createLogger()),
  DevTools.instrument()
)(createStore);

つまり、Ajaxをするならこのタイミング

React Router

URLのパスとcomponentの構造をマッピングするライブラリ

<Route path="/" component="App">
  <IndexRoute component="Home">
  <Route path="home" component="Home"/>
  <Route path="profile" component="Profile"/>
</Route>

rackt/react-router

URLを変更するには、

import {Link} from 'react-router';

<Link to='/home'>Home</Link>

URLが変更されたときに、パスを取得するには、

function mapStateToProps(state) {
  return {
    pathname: state.router.location.pathname
  }
}

のように渡ってくるので、これをpropsとして使う

プログラムからroutingする場合は、

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { pushState } from 'redux-router';

class SearchBox extends Component {
   handleClick() {
       this.props.pushState(null, '/to/other/route');
   }

   render() {
      // ........
   }
}

function selector(state) { //... }
export default connect(
  selector,

  // Use an action creator for navigation
  { pushState }
)(SearchBox);

How/where can I access router.transitionTo? · Issue #79 · rackt/redux-router

こうすればいける

React初心者のためのreact-routerの使い方 - ハッカーを目指す白Tのブログ

Redux Router

react-routerの機能は十分だが、現在表示しているページという”状態”がアプリケーションに登場する これもできればstoreに押し込めたい。ということで登場したライブラリ

JavaScript - reduxを試してみた(4日目) - redux-react-routerを試す - Qiita

rackt/redux-router

故に、configureStore.jsで

const finalCreateStore = compose(
  applyMiddleware(thunk, logger()),
  reduxReactRouter({ routes, createHistory }),
  applyMiddleware(createLogger()),
  DevTools.instrument()
)(createStore)

のように渡す必要がある

はじめ、なんでroutingをstore部分に書いているのかわからなかったんですが、storeで管理して、routingが変更されたらそれがstoreから渡ってくるようにしたいがためにやっていたんですね

middleware

middleware はまだあまり調べられていませんが、 redux/examples/real-world at master · rackt/redux を参考にシンプルバージョンで解説してみます

このサンプルでは、apiにリクエストする部分をmiddlewareとして作っているようで、その呼び出し方も特徴的です

まずはmiddlewareのapi.jsから(すごくシンプルにしてあります)

api.js

function callApi() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('success')
    }, 1000)
  })
}

// Action key that carries API call info interpreted by this Redux middleware.
export const CALL_API = Symbol('hisasann')

// A Redux middleware that interprets actions with CALL_API info specified.
// Performs the call and promises when such actions are dispatched.
export default store => next => action => {
  console.log('api: ', action);
  const callAPI = action[CALL_API]

  if (typeof callAPI === 'undefined') {
    return next(action)
  }

  const { types } = callAPI

  function actionWith(data) {
    const finalAction = Object.assign({}, action, data)
    delete finalAction[CALL_API]
    return finalAction
  }

  const [ requestType, successType, failureType ] = types
  next(actionWith({ type: requestType }))

  return callApi().then(
    (response) => {
      next(actionWith({
        type: successType,
        message: response
      }))
    },
    (error) => {
      next(actionWith({
        type: failureType
      }))
    })
}

重要な箇所は、

export default store => next => action => {

で、この形式がmiddlewareを自作するときに必要な関数になります

configureStore.js

そしてこの関数を applyMiddleware に渡すことで、一連のreduxフローの中に組み込むことができるようになります

import api from '../middleware/api'

const finalCreateStore = compose(
  applyMiddleware(thunk, api)
)(createStore)

action.js

はじめ、この部分の理解が難しかったです

return で渡しているものはただのオブジェクトで、実際にリクエスト処理を呼び出している感がなかったからです

function fetchUser() {
  return {
    [CALL_API]: {
      types: [ USER_REQUEST, USER_SUCCESS, USER_FAILURE ]
    }
  }
}

export function loadUser() {
  return (dispatch, getState) => {
    return dispatch(fetchUser())
  }
}

実際に読み解くとシンプルで、applyMiddlewareで登録したmiddlewareはactionでreturnされた後に介入できるので、

そのタイミングで、先ほどの

export default store => next => action => {

この部分が呼ばれるので、あとは、[CALL_API] で判定して必要な処理を実行しています

middlewareのタイミングでapi処理をするのが適切なのか、ちょっとまだわからないですが、reduxのアーキテクチャ上、確かにここでやるのもアリな気がします

雑感

データフローをone wayにしたアーキテクチャパターンは、昔からありましたが、キレイに交通整理し、必要な部分を必要なだけ隠蔽したというのがReduxの印象です

使い方が特殊なところもあるので、まずは慣れる、という感じでしょうか

今後調べたことをこのリポジトリに追記していきます

参考記事

reduxとfluxを比べてみたときの個人的な感想

JavaScript - reduxを試してみた(4日目) - redux-react-routerを試す - Qiita

ReduxとES6でReact.jsのチュートリアルの写経 - bokuweb.me

reduxを試してみた(5日目) - ajaxを使ってUIを構築する(reduxにおける非同期の制御) - Qiita

Others

Mac風のUIを作れるフレームワーク

Photon

Wordpress.comがReact/Reduxを使って全面アップデートしたOSSプロジェクト

Automattic/wp-calypso

( ・∀・)イイ!!

hisasann/electron-react-redux-boilerplate