Help us understand the problem. What is going on with this article?

React Native - Redux (part1 - todoApp)

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away