React-Native導入（redux）


この記事は「[IDOM Engineer Advent Calendar 2017](https://qiita.com/advent-calendar/2017/idom-engineer)」の24日目の記事です。

はじめまして、IDOMの[@hoang.vx](https://qiita.com/hoangvx)です。

# はじめに

現在はスマホの時代です。毎日、人々がスマホでニュースを読んだり、ゲームをやったり、仕事したりしています。そのため、アプリの開発をする人が多く人気になっています。
ですが、AndroidとiOSに同時にするとなるとそれぞれ用のPlatformで開発しないといけないです。本記事ではソースを1回だけ書いてAndroidとiOSに対応できる方法を紹介したいと思います。

### React-Native(RN)とは何でしょう

ReactはFacebook社が開発したJavaScriptのフレームワークであり、React Native(略称:RN)はそれをモバイルで使えるようにしたものです。

RNを利用するとJavascriptからモバイルアプリ（Android, iOS）をビルドできます。

RNが本当のモバイルアプリをビルドすること。”モバイルウェブアプリ、HTML5アプリ、ハイブリッドアプリ”ではありません。

RNで普通の`Native`コードも`Inject`することができます。

ホームページ：[React Native By Facebook](https://facebook.github.io/react-native/)

### Reduxはなんでしょう

Reduxは、stateを管理するです。

![](https://camo.githubusercontent.com/9de527b9432cc9244dc600875b46b43311918b59/68747470733a2f2f73332e616d617a6f6e6177732e636f6d2f6d656469612d702e736c69642e65732f75706c6f6164732f3336343831322f696d616765732f323438343739302f415243482d5265647578322d657874656e6465642d7265616c2d6465636c657261746976652e676966)

React以外にもAngularJSやjQueryなどと併せて使用することもできますが、Reactが一番相性がいいです。

Reduxのコンセプトは`actions` `reducers` `store`でUIのステージを管理すること。

+ `actions`：アクションはビューのイベントからデータを`store`に送信する
+ `reducers`：アクションはウェブのイベントを発生するときでデータを送信するんですが、ビューのステージを変更することは`reducers`でやるべきことです。
+ `store`：`actions`は何か起きます、`reducers`はステージを更新します。`store`では以上の2部分をマッピングする部分です。

# 簡単TODOサンプル

では、実際のプロジェクトを作成してみましょう。

## 準備環境
+ MacOS（私の場合）
+ [NodeJS](https://nodejs.org/en/download/)
+ [React-Native CLI](https://facebook.github.io/react-native/docs/getting-started.html)
	
## プロジェクト作成

React Native CLIでプロジェクトを作成します。

```
$ react-native init todolist
$ cd todolist
$ react-native run-ios
```
	
以下の画面が出てきます。

<img src=https://qiita-image-store.s3.amazonaws.com/0/181951/c746b342-79b4-3318-79e1-1c42324ce7fb.png "Simulator Screen Shot 2017.11.30 15.58.25.png width=400px>


アプリをリロードするには`cmd + R`を押します。
`cmd + D`を押すと開発（Dev）メニューが開きます。

<img src=https://qiita-image-store.s3.amazonaws.com/0/181951/5c0dbc69-5616-1cff-830f-30ada764ccb9.png "Simulator Screen Shot 2017.11.30 15.58.42.png "Simulator Screen Shot 2017.11.30 15.58.25.png width=400px>

プロジェクトの構成説明：

```
$ ll
total 336
-rw-r--r--    1   1164 11 30 15:39 App.js
drwxr-xr-x    3     96 11 30 15:39 __tests__
drwxr-xr-x   10    320 11 30 15:39 android
-rw-r--r--    1     53 11 30 15:39 app.json
-rw-r--r--    1    124 11 30 15:39 index.js
drwxr-xr-x    8    256 11 30 15:42 ios
drwxr-xr-x  601  19232 11 30 15:41 node_modules
-rw-r--r--    1    425 11 30 15:41 package.json
-rw-r--r--    1 154665 11 30 15:39 yarn.lock
```

+ App.js: アプリの実行コード
+ index.js: ビルドファイル
+ android/: Androidアプリソースのディレクトリ（Android Studioで開けます）
+ ios/: iOSアプリソースのディレクトリ（XCodeで開けます）
+ node_modules: `yarn`か`npm`でインストールするパッケージ

ソースを修正してみましょう。

```App.js
import React, { Component } from 'react';
import {
  Platform,
  StyleSheet,
  Text,
  View
} from 'react-native';

export default class App extends Component {
  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.welcome}>
          Hello world! I am a newbie!
        </Text>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  welcome: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
  }
});
```
アプリをリロード（`cmd + R`）してから、以下のような画面が表示されます。

<img src=https://qiita-image-store.s3.amazonaws.com/0/181951/b16f87bb-87ef-31ca-3a62-43f0b70c365a.png width=400 />

## Reduxを追加しましょう

`yarn`か`npm`で必要なパッケージをインストール

```bash
$ yarn add redux react-redux
```

プロジェクト構成：新しい`app`ディレクトリを作成して、redux用のディレクトリを作成

```bash
app$ tree
.
├── actions
├── lib
├── reducers
└── store
```

はじめに、サンプルタスク一覧を追加してみます。

```app/containers/Main.js
// 必要なリソース追加する
import React, { Component } from 'react';
import {
  Text,
  View,
  FlatList,
  TextInput,
  StyleSheet,
  Platform,
} from 'react-native';

class MainContainer extends Component {
  render() {
  	// データソース定義
    let todos = [
      { id: 1, title: "create some actions" },
      { id: 2, title: "create some reducer" },
      { id: 3, title: "create store" }
    ];
    // この部分はビューをレンダーです。
    return (
      <View style={styles.container}>
        <FlatList data={todos}
                  renderItem={this._renderItems}
                  keyExtractor={(item, index) => index} />
      </View>
    );
  }

　// このメソッドでは一覧の各アイテムをレンダーするです。
  _renderItems({ item }) {
    return (
      <View>
        <Text>{item.title}</Text>
      </View>
    )
  }
}

// ビューのスタイル修正
const styles = StyleSheet.create({
  container: {
    marginTop: Platform.OS === "ios" ? 20 : 0
  }
});

// このContainerを利用できるためエクスポートします
export default MainContainer;
```

```index.js
import { AppRegistry } from 'react-native';

// MainContainerを利用するため
import App from './app/containers/Main';

AppRegistry.registerComponent('todolist', () => App);

```

すると、以下の画面が表示されます。

<img src=https://qiita-image-store.s3.amazonaws.com/0/181951/001a28c2-a94f-c3a9-fcd9-bfe2f528a57f.png width=400 />

#### 以下から `redux` のコンポーネントを追加していきます。

typesファイル追加する。このファイルではアプリのイベント名あるいはやることを名前付けて管理します。

```actions/types.js
export const ADD_TODO = "ADD_TODO"
export const TOGGLE_TODO = "TOGGLE_TODO"
export const REMOVE_TODO = "REMOVE_TODO"
export const SET_VISIBILITY_FILTER = "SET_VISIBILITY_FILTER"
```

actionファイル追加する。このファイルは `actions` というメソッドを書きます。（これはイベント際に呼びます）

```actions/index.js
/**
 * 利用する変数をインポートする
 */
import {
  ADD_TODO,
  TOGGLE_TODO,
  SET_VISIBILITY_FILTER
} from './types'

let nextTodoId = 0

/**
 * タスク追加イベント
 * @param {string} text 
 */
export const addTodo = text => {
  return {
    type: 'ADD_TODO',
    id: nextTodoId++,
    text
  }
}

/**
 * フィルターする
 * @param {filter} filter 
 */
export const setVisibilityFilter = filter => {
  return {
    type: 'SET_VISIBILITY_FILTER',
    filter
  }
}

/**
 * タスクのステータス変更
 * @param {number} id 
 */
export const toggleTodo = id => {
  return {
    type: 'TOGGLE_TODO',
    id
  }
}
```

reducersを追加
+ todos.jsファイルはタスク一覧に関する処理
+ visibilityFilter.jsファイルは一覧のフィルターです
+ index.jsファイルは上の2ファイルをコンパイルして一つの `reducer` を返します

```todos.js
import {
  ADD_TODO,
  TOGGLE_TODO
} from '../actions/types'

/**
 * アクションによる次のステートを返す
 * @param state // タスク一覧
 * @param action // actions/index.jsから
 */
const todos = (state = [], action) => {
  switch (action.type) {
    case ADD_TODO:
      return [
        ...state,
        {
          id: action.id,
          text: action.text,
          completed: false
        }
      ]
    case TOGGLE_TODO:
      return state.map(todo =>
        (todo.id === action.id) 
          ? {...todo, completed: !todo.completed}
          : todo
      )
    default:
      return state
  }
}

export default todos
```

```visibilityFilter.js
import {
  SET_VISIBILITY_FILTER
} from '../actions/types'

/**
 * 次のフィルター返す
 * @param state // 現在のフィルターキー
 * @param action // actions/index.jsから
 */
const visibilityFilter = (state = 'SHOW_ALL', action) => {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return action.filter
    default:
      return state
  }
}

export default visibilityFilter

```

```index.js
import { combineReducers } from 'redux'
import todos from './todos'
import visibilityFilter from './visibilityFilter'

/**
 * Reducerコンパイル
 */
const todoApp = combineReducers({
  todos,
  visibilityFilter
})

export default todoApp
```

store追加。このファイルは　`actions` `reducers` を連携する部分ですね。そして、`middleware`などを使う場合このファイルに追加します。
+ `redux-logger` テスト、デバッグ際にログを書き込む
+ `redux-thunk` ajax, asyncメソッドを使う場合

```store/index.js
import { createStore, applyMiddleware, combineReducers, compose } from 'redux'
import reducers from '../reducers'
import { createLogger } from 'redux-logger'
import thunkMiddleware from 'redux-thunk'

const loggerMiddleware = createLogger({ predicate: (getState, action) => __DEV__ });

/**
 * Store設定、Middlware設定
 * @param {Object} initialState 
 */
const configureStore = initialState => {
  const enhancer = compose(
    applyMiddleware(
      thunkMiddleware,
      loggerMiddleware
    ),
  );
  return createStore(reducers, initialState, enhancer);
}

// initialStateは{}です。
const store = configureStore({});

export default store
```

ここまで全て `redux` コンポーネントを作成しましたが、`redux` と `react` を連携する設定が必要です。

```todolist/index.js
import React from 'react'
import { AppRegistry } from 'react-native'
import { Provider } from 'react-redux'
import store from './app/store'

// MainContainerを利用するため
import MainComponent from './app/components/Main'

/**
 * Provideを使ってreduxとreact連携する
 */
const App = () => {
  return (
    <Provider store={store}>
      <MainComponent />
    </Provider>
  )
}

AppRegistry.registerComponent('todolist', () => App);

```

次は画面コンポーネントを追加
便利なUIコンポーネントライブラリー追加

```bash
npm i --save react-native-vector-icons
npm i --save react-native-elements
react-native link
```

```components/Main.js
// 必要なリソース追加する
import React, { Component } from 'react';
import {
  View,
  StyleSheet,
  Platform
} from 'react-native';

import VisibleTodoList from '../containers/VisibleTodoList'
import AddTodo from '../containers/AddTodo'

class MainContainer extends Component {
  render() {
    // この部分はビューをレンダーです。
    return (
      <View style={styles.container}>
        <VisibleTodoList />
        <AddTodo />
      </View>
    );
  }
}

// ビューのスタイル修正
const styles = StyleSheet.create({
  container: {
    marginTop: Platform.OS === "ios" ? 20 : 0,
    flex: 1,
    justifyContent: "space-between",
    flexDirection: "column",
    backgroundColor: "#1D9FF2"
  }
});

// このContainerを利用できるためエクスポートします
export default MainContainer;
```

```containers/AddTodo.js
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { addTodo } from '../actions'

import {
  View,
  Text,
  TouchableHighlight,
  TextInput,
  StyleSheet
} from 'react-native';


class AddTodo extends Component {
  constructor(props) {
    super(props)
    this.state = {
      todo: ""
    }
  }

  /**
   * Add todo
   */
  _addTodo () {
    // actions
    this.props.addTodo(this.state.todo)
    this.setState({ todo: "" })
  }

  render() {
    return (
      <View style={style.inputForm}>
        <TextInput
          style={style.inputField}
          value={this.state.todo} onChangeText={(todo) => this.setState({ todo })} />
        <TouchableHighlight style={style.btn} onPress={() => this._addTodo()}>
          <Text style={style.btn_text}>Add</Text>
        </TouchableHighlight>
      </View>
    )
  }
}

/**
 * Redux: mapping action to props
 * @param dispatch 
 */
const mapDispatchToProps = dispatch => {
  return {
    addTodo: text => {
      dispatch(addTodo(text))
    }
  }
}

const style = StyleSheet.create({
  inputForm: {
    flex: 0,
    flexDirection: "row",
    padding: 5,
    shadowOffset:{  width: 2,  height: 2,  },
    shadowColor: '#F0FBFF',
    shadowOpacity: 1.0,
  },
  inputField: {
    flex: 1,
    padding: 6,
    backgroundColor: "#F0FBFF",
    borderRadius: 5
  },
  btn: {
    borderRadius: 5,
    backgroundColor: "#167ACC",
    marginHorizontal: 10,
    paddingHorizontal: 10,
    paddingVertical: 5,
    shadowOffset:{  width: 1,  height: 1,  },
    shadowColor: '#F0FBFF',
    shadowOpacity: 0.5,
  },
  btn_text: {
    color: "#F0FBFF",
    fontWeight: "bold"
  }
});

// state null ( {} )
export default connect(() => { return {} }, mapDispatchToProps)(AddTodo)
```

```containers/VisibleTodoList.js
import { connect } from 'react-redux'
import { toggleTodo, setVisibilityFilter } from '../actions'
import TodoList from '../components/TodoList'

/**
 * filter todos
 * @param todos 
 * @param filter 
 */
const getVisibleTodos = (todos, filter) => {
  switch (filter) {
    case 'SHOW_COMPLETED':
      return todos.filter(t => t.completed)
    case 'SHOW_ACTIVE':
      return todos.filter(t => !t.completed)
    case 'SHOW_ALL':
    default:
      return todos
  }
}

/**
 * Redux: Map states to props
 * @param state 
 */
const mapStateToProps = state => {
  return {
    todos: getVisibleTodos(state.todos, state.visibilityFilter)
  }
}

/**
 * Redux: Map actions to props
 * @param dispatch 
 */
const mapDispatchToProps = dispatch => {
  return {
    onTodoClick: id => {
      dispatch(toggleTodo(id))
    },
    filterTodos: filter => {
      dispatch(setVisibilityFilter(filter))
    }
  }
}

// Map props for TodoList
const VisibleTodoList = connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList)

export default VisibleTodoList
```

```components/TodoList.js
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import Todo from './Todo'
import { List, ButtonGroup } from 'react-native-elements'

import {
  FlatList,
  StyleSheet,
  View,
  ScrollView
} from 'react-native';

class TodoList extends Component {
  constructor (props) {
    super(props)
    this.state = {
      selectedIndex: 0
    }
    this.updateIndex = this.updateIndex.bind(this)
  }

  /**
   * Filter todos
   * @param selectedIndex 
   */
  updateIndex (selectedIndex) {
    const filter = [
      { index: 0, filter: "SHOW_ALL" },
      { index: 1, filter: "SHOW_ACTIVE" },
      { index: 2, filter: "SHOW_COMPLETED" }
    ].find(({ index, filter }) => index === selectedIndex).filter
    this.props.filterTodos(filter)
    this.setState({selectedIndex})
  }
  
  render() {
    let { todos, onTodoClick } = this.props
    const buttons = ['ALL', 'ACTIVE', 'COMPLETE']
    const { selectedIndex } = this.state
    return (
      <View>
        <ButtonGroup
          onPress={this.updateIndex}
          selectedIndex={selectedIndex}
          buttons={buttons}
        />
        <ScrollView>
          <List containerStyle={{ marginTop: 0 }}>
            {
              todos.map(todo => (
                <Todo key={todo.id} {...todo} onClick={() => onTodoClick(todo.id)} />
              ))
            }
          </List>
        </ScrollView>
      </View>
    )
  }
}

const style = StyleSheet.create({
  list: {
    flex: 1,
    padding: 5
  }
});

export default TodoList
```

```components/Todo.js
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { CheckBox, ListItem } from 'react-native-elements'

import {
  View,
  Text,
  TouchableHighlight,
  StyleSheet
} from 'react-native';

class Todo extends Component {
  render() {
    let { onClick, completed, text } = this.props
    let completeStyle = completed ? { name: "done", color: "green" } : { name: "work" }
    return (
      <ListItem 
        title={text}
        rightIcon={completeStyle}
        onPressRightIcon={onClick}
      />
    )
  }
}

const style = StyleSheet.create({
  todo: {
    flexDirection: "row"
  }
});

export default Todo
```

ここまでで終わりです。
アプリを実行すると…

<img src=https://media.giphy.com/media/l49JNvMhogZ6LX6uc/giphy.gif width=400px/>


# まとめ

+ React-nativeで一回ソースを書いてからiOSとAndroidアプリを生成できます
+ Reduxを使うとstateをprops管理しやすいです。
+ React-nativeのソースではES6が使えますので便利です。

プロジェクトのソースです
https://github.com/hoangvx/react-native-redux

### 以上です。

> 外国人で日本語はそんなに正しくないですね。〜＿〜　誰か直してくれば嬉しです。(；´∀｀)