ReactとReact Nativeでコードを共通化し，web / android / iOS (+PC)クロスデバイス開発

React, React Native, Reduxを使って，web, android, iOSアプリのコードを出来る限り共通化します．
ReactとReduxを組み合わせて使う場合，見た目を司るPresentational componentとロジックを司るContainer componentにコードを分離します( http://qiita.com/tuttieee/items/a3ca7d9d415049d02d60 )．
そこで，Presentational componentは各デバイス固有のコードにし，Container componentは共通化することでロジックをデバイス依存のコードから切り離し一つにまとめます．actionやreducerなども共通化できるので，結果としてロジックはすべて共通化され，Viewのみがプラットフォーム固有のコードになります．

ここでは，よくあるTodoアプリの超劣化版として，Todoを「追加できるだけ」のアプリを作ってみます．
ロジックの共通化のための設計にフォーカスするので，アプリ自体は作りこみません．あしからず．

こんなしょうもないアプリですが，React nativeとReact両方で動きます．
<img style="max-height: 400px" alt="Simulator Screen Shot 2016.05.18 0.03.17.png" src="https://qiita-image-store.s3.amazonaws.com/0/65141/a78068b7-d0e9-6264-f0cd-84c334a4cd68.png">
<img style="max-height: 400px" alt="スクリーンショット 2016-05-18 0.06.06.png" src="https://qiita-image-store.s3.amazonaws.com/0/65141/c2ac467b-4ffb-da47-5c84-5b568843aad0.png">


Webで動くなら，ということでおまけでElectronにも載せてみました
<img height="400" alt="スクリーンショット 2016-05-18 0.04.31.png" src="https://qiita-image-store.s3.amazonaws.com/0/65141/778da697-4851-c813-1ab1-ce9d9f483521.png">



コードはこちらです: https://github.com/tuttieee/ReactCrossDeviceTodoExample
READMEのとおりにやれば動かせるはずです．

# Structure
React nativeの雛形からスタートし，`src`ディレクトリを新たに作ってそこにコードを書いていきます．
ビルドにはgulpやwebpackを使っているので，そのファイルも追加されています．

```
.
├── android
├── ios
├── node_modules
├── public
├── src
├── gulpfile.js
├── index.android.js
├── index.electron.js
├── index.ios.js
├── npm-debug.log
├── package.json
└── webpack.config.js
```

`src`以下はこんな感じです．

```
./src/
├── actions
│   └── index.js
├── connects
│   └── TodoApp.react.js
├── native
│   ├── components
│   │   ├── App.react.js
│   │   └── TodoApp.react.js
│   └── containers
│       └── TodoApp.react.js
├── store
│   ├── reducers
│   │   ├── index.js
│   │   └── todos.js
│   └── configureStore.js
└── web
    ├── components
    │   ├── App.react.js
    │   └── TodoApp.react.js
    ├── containers
    │   └── TodoApp.react.js
    ├── app.js
    └── index.html
```

このとおり，action, store, reducerはreduxアプリケーションのよくある形です．
一方，components, containersが`native`と`web`に分かれていて，普通は見ない`connects`というディレクトリがあります．

# ポイント
## Container components
コードをすべて解説するのは大変なので要点だけ書きます（action, store, reducerは普通にreduxでTodoアプリを作るのと同じ感じです）．

ポイントは，containerを共通化する部分です．
`connects/TodoApp.react.js`, `native/containers/TodoApp.react.js`, `web/containers/TodoApp.react.js`を見てみます．

```js:src/connects/TodoApp.react.js
import { connect } from 'react-redux'
import {
  addTodo,
} from '../actions'

const mapStateToProps = (state) => {
  return {
    todos: state.todos,
  }
}

const mapDispatchToProps = (dispatch) => {
  return {
    addTodo: text => dispatch(addTodo(text))
  }
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)
```

```js:src/native/containers/TodoApp.react.js
import TodoApp from '../components/TodoApp.react'
import connect from '../../connects/TodoApp.react'

export default connect(TodoApp)
```

```js:src/web/containers/TodoApp.react.js
import TodoApp from '../components/TodoApp.react'
import connect from '../../connects/TodoApp.react'

export default connect(TodoApp)
```

このように，通常であれば一つのcontainerファイルにまとめられる部分が，

- `mapStateToProps`と`mapDispatchToProps`を引数にして`connect()`を呼び，その返り値をExportするモジュール
- 上記を受け取って，Presentational componentに適用するモジュール

に分かれます．前者は共通ファイルで，`connects`以下に置いておき，後者をnativeとwebの両方に用意してそれぞれのプラットフォーム用のPresentational componentを読み込むことで，各プラットフォームに対応します．

もちろん，container componentでも共通化できないものがでてきたら，native / webそれぞれの下のcontainers以下に固有のcontainer componentを作ってしまえば対応できます．

また，見てお分かりの通り，`src/native/containers/TodoApp.react.js`と`src/web/containers/TodoApp.react.js`は全く同じ内容です．
別ファイルとすることで，ReactでビルドするときとReact nativeでビルドするときで，お互い共通化できないモジュールを読み込むことを防いでいるのですが，同じ内容のファイルが２つあるというのはよろしくないですね…
ここはどうにかしたいところです．（シンボリックリンクにしてみたりしましたが，うまくいきませんでした）



## Presentational components
参考までに，各プラットフォーム向けのPresentational componentは以下のようになっています．

```js:src/native/components/TodoApp.react.js
import React from 'react'
import {
  Component,
  StyleSheet,
  View,
  Text,
  TextInput,
  ListView,
} from 'react-native'

const styles = StyleSheet.create({
  container: {
    marginTop: 38
  }
})

export default class extends Component {
  constructor(props) {
    super(props)

    let ds = new ListView.DataSource({
      rowHasChanged: (r1, r2) => r1 !== r2
    })

    this.state = {
      ds: ds.cloneWithRows(props.todos || []),
      addTodoText: '',
    }
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.todos !== this.props.todos) {
      this.setState({
        ds: this.state.ds.cloneWithRows(nextProps.todos || [])
      })
    }
  }

  render() {
    const {
      addTodo,
    } = this.props

    return (
      <View style={styles.container}>
        <ListView
          dataSource={this.state.ds}
          renderRow={(rowData) => <Text>{ rowData.text }</Text>}
        />
        <TextInput
          style={{height: 40, borderColor: 'gray', borderWidth: 1}}
          onChangeText={addTodoText => this.setState({addTodoText})}
          value={this.state.addTodoText}
        />
        <Text onPress={() => {
          addTodo(this.state.addTodoText)
          this.setState({addTodoText: ''})
        }}>ADD</Text>
      </View>
    )
  }
}
```

```js:src/web/components/TodoApp.react.js
import React from 'react'

export default React.createClass({
  render() {
    const {
      todos,
      addTodo,
    } = this.props

    return (
      <div>
        <ul>
        {todos.map((todo, i) =>
          <li key={i}>{ todo.text }</li>
        )}
        </ul>

        <button onClick={() => {
          addTodo(this.refs.todo_text.value)
          this.refs.todo_text.value = ''
        }}>ADD</button>
        <input type="text" ref='todo_text'/>
      </div>
    )
  }
})
```

このように，Presentational componentは各プラットフォーム固有のUIパーツを使って，全然違うコードになります．

# Electron
web版ができればelectronが使える！ということで，electronでも動かしてみました．
`index.electron.js`がエントリポイントになっています．

# まとめ
ロジックは共通化，Viewはプラットフォーム固有化することで，
web / iOS / android / PC (windows, OSX, linux) のクロスプラットフォーム開発ができそうです．

ReactやReact nativeの哲学は`Learn once, write anywhere.`であって`Write once, run anywhere`ではないので，やり過ぎは良くないかもしれませんが…

リポジトリ（再掲）：
https://github.com/tuttieee/ReactCrossDeviceTodoExample

# 参考
http://jkaufman.io/react-web-native-codesharing/
こちらも似たようなことしていますが，containerの共通化まではしていません．この辺の塩梅は好みですかね．
この記事は各プラットフォーム向けのビルドスクリプトが載ってるのでそこも参考になりそうです．