LoginSignup
185

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-05-17

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両方で動きます.
Simulator Screen Shot 2016.05.18 0.03.17.png
スクリーンショット 2016-05-18 0.06.06.png

Webで動くなら,ということでおまけでElectronにも載せてみました
スクリーンショット 2016-05-18 0.04.31.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がnativewebに分かれていて,普通は見ないconnectsというディレクトリがあります.

ポイント

Container components

コードをすべて解説するのは大変なので要点だけ書きます(action, store, reducerは普通にreduxでTodoアプリを作るのと同じ感じです).

ポイントは,containerを共通化する部分です.
connects/TodoApp.react.js, native/containers/TodoApp.react.js, web/containers/TodoApp.react.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
)
src/native/containers/TodoApp.react.js
import TodoApp from '../components/TodoApp.react'
import connect from '../../connects/TodoApp.react'

export default connect(TodoApp)
src/web/containers/TodoApp.react.js
import TodoApp from '../components/TodoApp.react'
import connect from '../../connects/TodoApp.react'

export default connect(TodoApp)

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

  • mapStateToPropsmapDispatchToPropsを引数にしてconnect()を呼び,その返り値をExportするモジュール
  • 上記を受け取って,Presentational componentに適用するモジュール

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

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

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

Presentational components

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

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>
    )
  }
}
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の共通化まではしていません.この辺の塩梅は好みですかね.
この記事は各プラットフォーム向けのビルドスクリプトが載ってるのでそこも参考になりそうです.

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
185