React Native - Redux (part1 - todoApp)

一昨日、昨日に引き続きFluxです。今日は2015年12月時点でのFluxアーキテクチャの本命でGithub starが１万を超えるHotなReduxについてReact Nativeでどうやるかを説明します。


# Redux

ReduxはFluxアーキテクチャの一種で, Storeに1つのStateを持ち、それがアプリケーションの内部状態となります。React Nativeでは、ReduxのStateでアプリに内部状態を持たせることができるので、例えば、どのタブを開いてどの情報を持っているかが１つのStateを見るだけでわかります。これはテストで重宝します。結合テスト、E2EテストなどでStateを照らし合わせることで、コード内でアプリの状態がテストできます。

ReduxはReactだけでなくAngular等でも使えるようです。

[Egghead.io](https://egghead.io/series/getting-started-with-redux)でReduxの作者のDan氏が動画のチュートリアルを公開してます。無料で英語も容易なので見てみると良いと思います。


# Redux with React Native

今回はReact NativeでReduxの[TodoアプリのExample](http://rackt.org/redux/docs/basics/ExampleTodoList.html)を実装してみようと思います。

![スクリーンショット 2015-12-08 22.11.21.png](https://qiita-image-store.s3.amazonaws.com/0/13663/6a544603-f176-930d-772e-89fa6d460fa5.png)


Githubにソースコードがあります。
https://github.com/shohey1226/ReactNativeReduxTodoApp

## Installation

インストールは気をつけないといけません。2015年12月８日時点で、react-nativeはreact0.14に対応していなく、reduxの最新バージョンが使えません。React Native0.14を使う必要があります。
したがって、`package.json`は下記のようなものになります。ファイルを編集後、`npm install`でインストールします。

```
// package.json
{
  "name": "ReactNativeReduxTodoApp",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "start": "react-native start"
  },
  "dependencies": {
    "react-native": "^0.14.1",
    "react-native-button": "^1.2.1",
    "react-redux": "^3.1.0",
    "redux": "^3.0.4"
  }
```

## Directory structure

```
index.ios.js
app/
├── actions.js
├── components
│   ├── AddTodo.js
│   ├── Footer.js
│   ├── Todo.js
│   └── TodoList.js
├── containers
│   └── App.js
└── reducers.js
```

actions.jsやreducers.jsは分割することもできます。

## コード

では一つずつコードの中身を見ていきます。

### index.ios.js

`react-redux/native`をimportします。reducersからstoreを作り、アプリに追加するイメージです。

```
import React from 'react-native';
import { createStore } from 'redux';
import { Provider } from 'react-redux/native';
import App from './app/containers/App';
import todoApp from './app/reducers';

let {
  AppRegistry,
  View
} = React;

let store = createStore(todoApp)
let ReactNativeReduxTodoApp = React.createClass({
  render: function() {
    return(
      <Provider store={store}>
       {() => <App />}
      </Provider>
    );
  }
});

AppRegistry.registerComponent('ReactNativeReduxTodoApp', () => ReactNativeReduxTodoApp);
```

### app/containers/App.js

トップレイヤーのコンポーネントです。子コンポーネントを持ちます。`actions.js`からstoreへのアクセスをするためのアクションを持ちます。connectメソッドを`react-redux/native`からimportし、このコンポーネントとstoreを関連付けます。

```
import React from 'react-native';
import { connect } from 'react-redux/native'
import { addTodo, completeTodo, setVisibilityFilter, VisibilityFilters } from '../actions'
import AddTodo from '../components/AddTodo'
import TodoList from '../components/TodoList'
import Footer from '../components/Footer'

var ReactNativeReduxTodoApp = React.createClass({

  render: function() {
    const { dispatch, visibleTodos, visibilityFilter } = this.props;
    return (
      <View style={styles.container}>
        <Text style={styles.welcome}>
          Welcome to React Native!
        </Text>
        <AddTodo
          onAddClick={text =>
            dispatch(addTodo(text))
          }
        />
        <TodoList
          todos={visibleTodos}
          onTodoClick={index =>
            dispatch(completeTodo(index))
          }
        />
        <Footer
          filter={visibilityFilter}
          onFilterChange={nextFilter =>
            dispatch(setVisibilityFilter(nextFilter))
          }
        />
      </View>
    );
  }
});
...
function selectTodos(todos, filter) {
  switch (filter) {
    case VisibilityFilters.SHOW_ALL:
      return todos
    case VisibilityFilters.SHOW_COMPLETED:
      return todos.filter(todo => todo.completed)
    case VisibilityFilters.SHOW_ACTIVE:
      return todos.filter(todo => !todo.completed)
  }
}

function select(state) {
  return {
    visibleTodos: selectTodos(state.todos, state.visibilityFilter),
    visibilityFilter: state.visibilityFilter
  }
}

export default connect(select)(ReactNativeReduxTodoApp);

```

### app/components/AddTodo.js

Todoの追加用コンポーネントです。`onAddClick`関数はpropとして親から渡されています。したがって、addボタンを押すと、props.onAddClick()が呼び出され、それは親にあるaction、という流れになります。

```
import React, { Text, View, TextInput} from 'react-native';
var Button = require('react-native-button');

let AddTodo = React.createClass({

  propTypes: {
    onAddClick: React.PropTypes.func.isRequired
  },

  getInitialState(){
    return { text: ""};
  },

  _handlePress(){
    this.props.onAddClick(this.state.text);
    this.setState({text: ""});
  },

  render(){
    return(
      <View>
        <TextInput
          style={{height: 40, width: 150, borderColor: 'gray', borderWidth: 1}}
          onChangeText={(text) => this.setState({text})}
          value={this.state.text}
        />
        <Button style={{color: 'green'}} onPress={this._handlePress}>
          Add
        </Button>
      </View>
    );
  }
});

export default AddTodo;
```

### app/actions.js

では、そのアクションの中身はどうなってるかというと、下記のようにfacebook/fluxでいうconstant(storeに伝えるための合言葉）を持ち、reducerに伝えます。reducerではなるべくビジネスロジックを持たない方が良く、コンポーネント内でも同様でなので、ビジネスロジックを入れるのはこのAction、またはActionでサービスクラスなどを作って呼んで使うかになると思います。この例では非常にシンプルなActoion Creatorしか持ちません。

```
*
 * action types
 */

export const ADD_TODO = 'ADD_TODO'
export const COMPLETE_TODO = 'COMPLETE_TODO'
export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER'

/*
 * other constants
 */

export const VisibilityFilters = {
  SHOW_ALL: 'SHOW_ALL',
  SHOW_COMPLETED: 'SHOW_COMPLETED',
  SHOW_ACTIVE: 'SHOW_ACTIVE'
}

/*
 * action creators
 */

export function addTodo(text) {
  return { type: ADD_TODO, text }
}

export function completeTodo(index) {
  return { type: COMPLETE_TODO, index }
}

export function setVisibilityFilter(filter) {
  return { type: SET_VISIBILITY_FILTER, filter }
}
```

### app/reducers.js

>Redux reduces the boilerplate of Flux stores considerably by describing the update logic as a function. A function is simpler than an object, and much simpler than a class.
**Reducerはobjectより、ましてやクラスよりシンプルなもの**

Redux目玉のreducerです。switch文でactionのconstantsを聞いています。そして、Actionが実行された時にStateの操作を行います。mutateさせないのがポイントになります。`Object.assign`を利用してコピーを作りstateをアップデートします。配列を利用することにより、stateがhistoryを持つように設計することもできます。



```
import { combineReducers } from 'redux'
import { ADD_TODO, COMPLETE_TODO, SET_VISIBILITY_FILTER, VisibilityFilters } from './actions'
const { SHOW_ALL } = VisibilityFilters

function visibilityFilter(state = SHOW_ALL, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return action.filter
    default:
      return state
  }
}

function todos(state = [], action) {
  switch (action.type) {
    case ADD_TODO:
      return [
        ...state,
        {
          text: action.text,
          completed: false
        }
      ]
    case COMPLETE_TODO:
      return [
        ...state.slice(0, action.index),
        Object.assign({}, state[action.index], {
          completed: true
        }),
        ...state.slice(action.index + 1)
      ]
    default:
      return state
  }
}

const todoApp = combineReducers({
  visibilityFilter,
  todos
})

export default todoApp
```

## Summary

今回はReduxのTodoAppをReact Nativeで作ってみました。基本的なReduxの説明になってしまいましたが、WebのReactと同様にReduxを使えることがわかったと思います。現時点でReact Nativeを始める場合、Reduxを採用することが多くなることが予測されるので、明日以降、もう少し掘り下げたいと思います。