React Native で Redux を導入する（React Native開発～第x回）

## 概要

React Native の開発で、Reduxを導入します。

今回は、簡単なUIで Redux のデータフローの動作を確認しましょう。

-----------------

## 前提条件

* JavaScript が読み書きできる。

* React Native の開発準備が整っていること。
  * [React Native の開発環境を整える（React Native開発実践～第0回） - Qiita](https://qiita.com/EBIHARA_kenji/items/2f6938c4fda7cecbeb19)

* TypeScript でプロジェクトが作成されている。

-----------------

## Redux とは

Redux は、ブラウザアプリでデータを state として管理するためのフレームワークです。

これは React Native のモバイルアプリ開発でも利用できます。

導入することで、複雑なアプリのデータの流れがわかりやすくなり、メンテナンス性が高まります。

React と合わせて使われることが多いですが、それぞれ独立したフレームワークなので、React 意外と組み合わせて使うことも可能です。

Redux について理解するには、下記のページが詳ので、一読するのをオススメします。

* [Redux入門【ダイジェスト版】10分で理解するReduxの基礎 - Qiita](https://qiita.com/kiita312/items/49a1f03445b19cf407b7)

-----------------

## Redux ライブラリの導入

yarn でプロジェクトにインストールします。

```powershell
> yarn add redux react-redux clone
```

react-redux は、React と Redux を接続するためのライブラリです。

clone は、指定したオブジェクトと同じ内容の別なオブジェクトを作成する(ディープクローンする)ライブラリです。

Redux の処理に利用します。

これらの TypeScript の型定義ファイルもインストールします。

```powershell
> yarn add --dev @types/redux @types/react-redux @types/clone
```
-----------------

## TSLint の導入

TypeScript はコンパイル処理で、構文エラーをチェックしてくれますが、TSLint はそれ以上の指定したコーディングルール（例えば インデントのスペースの数や {} の位置、１行の最大行数など）をチェックするツールです。

チームで開発する際、Lint を導入しておけば、ルールに則った統一されたコードが生成されやすいです。

yarn で導入します。

```powershell
> yarn add --dev tslint
```

ルールの定義ファイル `tslint.json` を作成します。 

```powershell
> yarn tslint --init
```

ルールは非常に多くのオプションがあります。

上のコマンドでは、おすすめのルールがすでに適用されたものが生成されます。

ここでは、これに加えて下記のルールを追加します。

```json:tslint.json
{
    "defaultSeverity": "error",
    "extends": [
        "tslint:recommended"
    ],
    "jsRules": {},
    "rules": {
        "quotemark": [ // 文字列リテラルのクォーテーション
            true,
            "single", //シングルクォーテーションを使う
            "jsx-double" // JSX のプロパティでは、ダブルクォーテーション
        ],
        "indent": [ // インデント
            true,
            "spaces", // スペースによるインデント 
            4 // スペースの数
        ]
    },
    "rulesDirectory": []
}
```

> TSLint ルール一覧: https://palantir.github.io/tslint/rules/

Visual Studio Code の TSLint 拡張を入れておくと、コーディング中にルール違反がわかります。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/263961/a382b1a1-7e3b-9d90-ed9e-91cdb41124c7.png)

## 各モジュールのディレクトリを作成する

Redux には、View(React Component)、Action、Reducer、State を言う要素に分けて開発します。それ毎にディレクトリを作るとソースコードの整理がしやすいです。

```txt
[project root]
 `- [src]
     |-[actions]
     |-[reducers]
     |-[states]
     |-[views]
        |-[components]
        `-[containers]
```

Power Shell の `New-Item` コマンドを利用すると、一気に作ることができます。

```powershell
> New-Item -Path "./src/actions", "./src/reducers", "./src/states/", "./src/views/components", "./src/views/containers" -itemType Directory
```

## Redux の動作を確認してみる

簡単な例で Redux の動作を確認します。

ここでは、画面で入力した文字を State で管理し、それを画面の別なコントロールに表示する、ということをやってみます。

## State の作成

まず、扱うデータの型を定義します。ここでは単純に、文字列を１つだけ扱うオブジェクトの型とします。

```typescript:src/states/IAppState.ts
interface IAppState {
    message: string;
}
export default IAppState;
```

## Action の作成

アクションは、画面で入力した文字を受け取り、アクションオブジェクトを返します。

State にわたすには、dispatch を使いますがこれは、Component から送ります。

```typescript:src/actions/AppAction.ts
import { Action } from 'redux';

/**
 * アクションを区別するための定数
 */
export const UPDATE_MESSAGE = 'UPDATE_MESSAGE';
/**
 * 変更したメッセージを Reducer に送るためのアクション
 */
export interface IUpdateMessageAction extends Action {
    message: string;
}
/**
 * メッセージを変更するアクションを作成する
 * @param message 変更するメッセージ
 */
export const createUpdateMessageAction = (message: string): IUpdateMessageAction => {
    return {
        message,
        type: UPDATE_MESSAGE,
    };
};
```

## Reducer の作成

Reducer は、送られた Action を使って State を更新する処理を書きます。

```typescript:src/reducers/AppReducer.ts
import clone from 'clone';
import { Action, Reducer } from 'redux';

import { IUpdateMessageAction, UPDATE_MESSAGE } from '../actions/AppActions';
import IAppState from '../states/IAppState';

/**
 * State の初期値
 */
const initState: IAppState = {
    message: '',
};

/**
 * Reducer 関数
 * @param state 現在のステート
 * @param action 渡されたアクション
 */
const appReducer: Reducer<IAppState> =
    (state: IAppState = initState, action: Action) => {
        let newState = state;
        switch (action.type) {
            case UPDATE_MESSAGE:
                {
                    // ステートを変更する場合は、別のオブジェクトを作成する
                    newState = clone(state);
                    const _action = action as IUpdateMessageAction;
                    newState.message = _action.message;
                }
                break;
            default:
                break;
        }
        // ここで返すオブジェクトが前回と異なるなら、関連する Component が再描画される。
        return newState;
    };

export default appReducer;
```


## Store の登録

Store は、State の管理と、Reducer による変更を受け取り、Component に伝える役割があります。

Reducer は複数ある場合が多いので、それを `combineReducers` でまとめて、 `createStore` に渡して Store を作成します。

まずは、アプリ全体の State を Interface で定義します。この中に、上で作成した State (子State) を含めます。

```typescript:src/IState.ts
import IAppState from './states/IAppState';

export default interface IState {
  app: IAppState;
}
```

Store の作成処理です。

```typescript:src/Store.ts
import { combineReducers, createStore } from 'redux';

import IState from './IState';
import appReducer from './reducers/AppReducer';

const reducers = combineReducers<IState>({
  app: appReducer,
});

const store = createStore(reducers);

export default store;
```

## Container Component の作成

ようやくになりますが、画面を設計します。  

画面は、React Component クラスとして定義します。

ここで作成する Component は Redux の Store と接続する特別なものとなります。これを Container といいます。

React は外部から値をプロパティとして受け取り、その値を表示したり、値によって表示条件を変えたりするなどします。

また、画面の入力やボタンをタップなどのイベントのときに、画面の値を帰る場合は Acton を生成し、 dispatch 関数で Reducer に送ります。

ここでは、仮想DOM（JSX） を書くため、拡張子を '.tsx' とする必要があります。

さきに、ルートにある `App.tsx` を削除し、`src/views/containers/App.tsx` を作成します。

```tsx:src/views/containers/App.tsx
import React, { Component } from 'react';
import { StyleProp, Text, TextInput, TextStyle, View, ViewStyle } from 'react-native';
import { connect } from 'react-redux';
import { Dispatch } from 'redux';
import { createUpdateMessageAction } from '../../actions/AppActions';
import IState from '../../IState';
import IAppState from '../../states/IAppState';

/**
 * UIのイベントで、Stateを変更するイベントハンドラを定義する
 */
interface IEvents {
    /**
     * メッセージが変更されたときの処理
     * @param message 変更後のメッセージ
     */
    onChangeMessage: (message: string) => void;
}

/**
 * この Container のプロパティを定義する
 */
interface IProps extends IEvents, IAppState {
}
/**
 * スタイルの定義
 */
const pageStyle: ViewStyle = {
    alignItems: 'center',
    flex: 1,
    justifyContent: 'center',
};
const inputStyle: StyleProp<TextStyle> = {
    borderColor: '#999',
    borderRadius: 5,
    borderWidth: 1,
    padding: 5,
    width: 200,
};
/**
 * サンプル画面 コンテナ
 */
class App extends Component<IProps> {
    /**
     * React の画面描画イベント
     */
    public render() {
        return (
            <View style={pageStyle}>
                <Text>はじめての React Native and Redux</Text>
                <TextInput
                    style={inputStyle}
                    placeholder="ここに値を入力してください"
                    onChangeText={this.props.onChangeMessage}>
                    {this.props.message}</TextInput>
                <Text>{this.props.message || ('ここに入力した文字が表示されます')}</Text>
            </View>
        );
    }
}
/**
 * ステートから、プロパティに値を適用する
 * @param state Redux のステート
 */
const mapStateToProps = (state: IState): IAppState => {
    return {
        message: state.app.message,
    };
};

/**
 * コンポーネントの UI イベントの処理を定義する
 * @param dispatch Reducer へ Action を送るための関数
 */
const mapDispatchToProps = (dispatch: Dispatch): IEvents => {
    return {
        onChangeMessage: (message: string) => {
            dispatch(createUpdateMessageAction(message));
        },
    };
};

// Redux の State と Component を接続する
export default connect(mapStateToProps, mapDispatchToProps)(App);
```

## スタートページを作って指定する

最後に、開始するときの Component を index.js で指定します。

Container は、必ず <Provider> という React-Redux モジュールが持っている コンポーネント の中で使わなくてはなりません。

ここでは下記のようにスタートページを作成します。

```tsx:src/View/StartPage.tsx
import React, { Component } from 'react';
import { Provider } from 'react-redux';
import store from '../Store';
import App from './containers/App';

export default class Start extends Component {
    public render() {
        return (
            // store プロパティには、上で作成した store オブジェクトを割り当てる
            <Provider store={store}>
                <App />
            </Provider>
        );
    }
}
```


index.js で Start.tsx を呼び出すように修正します。

```js:index.js
/** @format */

import { AppRegistry } from 'react-native';
import StartPage from './src/views/StartPage';
import { name as appName } from './app.json';

AppRegistry.registerComponent(appName, () => StartPage);
```

## 起動して確認してみる

VSCode デバッグ機能で、"Debug Android" を実行してみましょう。

![2019-02-07_09h40_02.gif](https://qiita-image-store.s3.amazonaws.com/0/263961/caa8cd66-3ee2-6271-1c8f-66b5dfd0ec81.gif)

## 次回

次回は、ページ遷移について書きたいと思います。