reactnative
redux

React Native - Redux (part1 - todoApp)

More than 1 year has passed since last update.

一昨日、昨日に引き続き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を実装してみようと思います。

スクリーンショット 2015-12-08 22.11.21.png

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を採用することが多くなることが予測されるので、明日以降、もう少し掘り下げたいと思います。