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