一昨日、昨日に引き続きFluxです。今日は2015年12月時点でのFluxアーキテクチャの本命でGithub starが1万を超えるHotなReduxについてReact Nativeでどうやるかを説明します。
Redux
ReduxはFluxアーキテクチャの一種で, Storeに1つのStateを持ち、それがアプリケーションの内部状態となります。React Nativeでは、ReduxのStateでアプリに内部状態を持たせることができるので、例えば、どのタブを開いてどの情報を持っているかが1つのStateを見るだけでわかります。これはテストで重宝します。結合テスト、E2EテストなどでStateを照らし合わせることで、コード内でアプリの状態がテストできます。
ReduxはReactだけでなくAngular等でも使えるようです。
Egghead.ioでReduxの作者のDan氏が動画のチュートリアルを公開してます。無料で英語も容易なので見てみると良いと思います。
Redux with React Native
今回はReact NativeでReduxのTodoアプリのExampleを実装してみようと思います。
Githubにソースコードがあります。
https://github.com/shohey1226/ReactNativeReduxTodoApp
Installation
インストールは気をつけないといけません。2015年12月8日時点で、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を採用することが多くなることが予測されるので、明日以降、もう少し掘り下げたいと思います。
