ReactNative（個人用）作業テンプレート


Reactに関する個人的テンプレート。

## 追記（2018年11月30日）

このテンプレートはreact-navigationのV2を利用しています。cloneしてnpm installする場合は問題ありませんが、新規で作成する場合は（V3の場合）、createAppContainerでNavigationをラップしてやる必要があります。

>サンプルコードを更新しました（git以外）。

## やること

下記を順を追って実装する。それぞれにgitのtagをつける。

* とりあえず表示 1-1
* StackNavigatorの利用 1-2
* Reduxの利用
  * stateの共有 1-3
  * メソッドの共有 1-4
* Thunkによる非同期APIリクエスト 1-5
* Sagaによる非同期APIリクエスト 1-6


完成済みのソースが[github](https://github.com/eizaburo/react-temp-basic)上にあります（ソースコードは更新されていません）。
また、各STEPに対応したtagが設定してあります。目的に応じてtagを指定してcloneしてもいいでしょう。

```bash
git clone -b 1-4 https://github.com/eizaburo/react-temp-basic
```

>reduxから利用したい場合は1-4をcloneすればいいでしょう。ただ、その状態だとno branchとなっているので、git checkout -b masterなどとしてから作業するのがよいでしょう。

## 準備

私はexpoを利用しています。インストールしていなければインストールします。

```bash
npm install -g expo
```

作業ディレクトリの作成して移動します。

```bash
expo init react-temp
cd react-temp
```

>ディレクトリ名は別に何でもいいです。

## とりあえず表示

### ディレクトリとファイルの準備

App.jsと同じ階層にscreensディレクトリを作成し、Home.jsファイルを作成します。

```bash
mkdir screens
touch screens/Home.js
```

### Home.js

Home.jsを実装します。画面の中心にHomeと表示するだけのものです。

```js
import React from 'react';
import { View, Text, Button } from 'react-native';

class Home extends React.Component {
    render() {
        return (
            <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
                <Text>Home</Text>
            </View>
        );
    }
}

export default Home;
```

### App.js

Home.jsを読み込み表示します。

```js
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';

import Home from './screens/Home';

export default class App extends React.Component {
  render() {
    return (
      <Home />
    );
  }
}
```

表示を確認しておきます。

```
expo start
```

専用のWebが立ち上げるのでお好みの環境で確認を。
私は[Run on iOS simulator]で確認しています。

## StackNavigatorの利用

続いてreact-navigationのStackNavigatorを実装してみます。StackNavigatorは一番シンプルなページ移動です（次へ、戻るという感じのやつ）。別にTabでもいいのですが、とりあえずStackで。

まずは必要なモジュールをインストールします。

```bash
npm install --save react-navigation
npm install
```

>react-navigationの後は必ず再度npm installしないとエラーになるようです。何で？

### ファイルの準備

移動先となる画面を作成します。ここではDetail.jsとします。

```bash
touch screens/Detail.js
```

### Detail.js

Ditail.jsを実装します。と言ってもHome.jsのHomeという要素がDetailになっただけです。

```js
import React from 'react';
import { View, Text, Button } from 'react-native';

class Detail extends React.Component {
    render() {
        return (
            <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
                <Text>Detail</Text>
            </View>
        );
    }
}

export default Detail;
```

### App.js

では、App.jsにて実際にStackNavigatorを設定していきます。
HomeとDetailページを設定し、screenに各ファイルを設定します。

```diff
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';

import Home from './screens/Home';
+import Detail from './screens/Detail';
+import { createStackNavigator, createAppContainer } from 'react-navigation';

+const Stack = createStackNavigator(
+  {
+    Home: {screen: Home},
+    Detail: {screen: Detail},
+  },
+  {
+    initialRouteName: 'Home'
+  }
+);

//v3より必要
+const AppContainer = createAppContainer(Stack);


export default function App() {
  return (
+    <AppContainer/>
  );
}
```

### Home.js

なお、HomeにはDetailに移動するボタンを設置します（戻るボタンは自動生成されます）。

```diff
import React from 'react';
import { View, Text, Button } from 'react-native';

class Home extends React.Component {
    render() {
        return (
            <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
                <Text>Home</Text>
+                <Button
+                    title='Link to Detail'
+                    onPress={() => this.props.navigation.navigate('Detail')}
+                />
            </View>
        );
    }
}

export default Home;
```

簡単ですが、以上です。

## Reduxの利用（Stateの共有）

2,3ページくらいなら値の共有はpropsの受け渡しでいいのですが、複数のページになるとReduxの利用は避けられません。

### モジュールのインストール

```bash
npm install --save redux react-redux
```

### フォルダとファイルの準備

必要なフォルダやファイルを準備します。
フォルダやファイルの配置には好みがありますが、reducers, actionsといったredux wayを踏襲した雰囲気にしてみます。

また、storeの作成用にcreateStore.jsという専用ファイルを作成することにします（複数のreducerやmiddlewareを制御しやすいので）。

```bash
touch createStore.js
mkdir reducers
touch reducers/userReducer.js
```

### createStore.js

今回はreducerを1つしか使いませんが、複数に対応できるようにcombineReducers()を利用して記述しておきます。
また、Middlewareについても後々利用するので、記述しておきます。

```js
import { createStore as reduxCreateStore, combineReducers, applyMiddleware } from 'redux';
import userReducer from './reducers/userReducer';

export default createStore = () => {
    const store = reduxCreateStore(
        combineReducers({
            userData: userReducer,
        }),
        applyMiddleware(
            //
        )
    );
    return store;
}
```

### userReducer.js

Userデータを保持するイメージなのでuserReducer.jsとしました。
内容は初期値を設定して、ただ返しているだけです。後々、actionでswitchするので、その部分も記述しています。

```js
const initialState = {
    user: {
        name: 'hoge',
        age: 33,
    }
}

const userReducer = (state = initialState, action) => {
    switch(action.type){
        default:
            return state;
    }
}

export default userReducer;
```

### App.js

App.jsにおいてReduxで値が共有できるよう、Providerタグで囲み、storeを設定します。

```diff
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';

import Home from './screens/Home';
import Detail from './screens/Detail';
import { createStackNavigator, createAppContainer } from 'react-navigation';

+import { Provider } from 'react-redux';
+import createStore from './createStore';

const Stack = createStackNavigator(
  {
    Home: {screen: Home},
    Detail: {screen: Detail},
  },
  {
    initialRouteName: 'Home'
  }
);

//v3より必要
const AppContainer = createAppContainer(Stack);

+const store = createStore();

export default function App() {
  return (
+    <Provider store={store}>
      <AppContainer/>
+    </Provider>
  );
}
```

### Home.js

Homeにおいて初期値が取得できるか試します。

```diff
import React from 'react';
import { View, Text, Button } from 'react-native';
import { connect } from 'react-redux';

class Home extends React.Component {
    render() {
        return (
            <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
                <Text>Home</Text>
                <Button
                    title='Link to Detail'
                    onPress={() => this.props.navigation.navigate('Detail')}
                />
+                <Text>{this.props.state.userData.user.name}</Text>
            </View>
        );
    }
}

+const mapStateToProps = state => (
+    {
+        state: state,
+    }
+);

+export default connect(mapStateToProps, null)(Home);

// export default Home;
```

### Detail.js

Detailにおいても同様に初期値が取得できるか試します。

```diff
import React from 'react';
import { View, Text, Button } from 'react-native';
import { connect } from 'react-redux';

class Detail extends React.Component {
    render() {
        return (
            <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
                <Text>Detail</Text>
+                <Text>{this.props.state.userData.user.name}</Text>
            </View>
        );
    }
}

+const mapStateToProps = state => (
+    {
+        state: state,
+    }
+);

+export default connect(mapStateToProps, null)(Detail);

// export default Detail;
```

どちらのページでも無事に表示できました。

## Reduxの利用（メソッドの共有）

値の共有はできましたが、スタティックな値を共有するだけでは意味がありません。
ここではnameをアップデートするメソッドを実装してみます。

### ディレクトリとファイルの準備

必要なディレクトリとファイルを作成します。

```bash
mkdir actions
touch actions/userAction.js
```

### userAction.js

actionを返すaction createrを定義します。

```js
export const updateName = name => (
    {
        type: 'UPDATE_NAME',
        name: name
    }
);
```

### userReducer.js

reducerを適切に変更します。
現在のnameを受け取ったnameで更新します。

```diff
const initialState = {
    user: {
        name: 'hoge',
        age: 33,
    }
}

const userReducer = (state = initialState, action) => {
    switch(action.type){
+        case 'UPDATE_NAME':
+            return Object.assign({}, state, {
+                user: {
+                    name: action.name
+                }
+            });
        default:
            return state;
    }
}

export default userReducer;
```

### Home.js

では、Homeで利用してみます。

```diff
import React from 'react';
import { View, Text, Button } from 'react-native';
import { connect } from 'react-redux';
+import { updateName } from '../actions/userAction';

class Home extends React.Component {
    render() {
        return (
            <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
                <Text>Home</Text>
                <Button
                    title='Link to Detail'
                    onPress={() => this.props.navigation.navigate('Detail')}
                />
                <Text>{this.props.state.userData.user.name}</Text>
+                <Button
+                    title='updateName'
+                    onPress={() => this.props.updateName('foo@Home')}
+                />
            </View>
        );
    }
}

const mapStateToProps = state => (
    {
        state: state,
    }
);

+const mapDispatchToProps = dispatch => (
+    {
+        updateName: (name) => dispatch(updateName(name)),
+    }
+);

+export default connect(mapStateToProps, mapDispatchToProps)(Home);

// export default Home;
```

うまく動きました。

## Thunkによる非同期処理

続いて外部APIからの値取得（というか非同期処理）を行ってみます。
まずは昔から定番のRedux-Thunkを利用してみます。

### モジュールのインストール

```bash
npm install --save redux-thunk
```

### createStore.js

createStore.jsにthunk利用のための記述を追加します。

```diff
import { createStore as reduxCreateStore, combineReducers, applyMiddleware } from 'redux';
import userReducer from './reducers/userReducer';
+import thunk from 'redux-thunk';

export default createStore = () => {
    const store = reduxCreateStore(
        combineReducers({
            userData: userReducer,
        }),
        applyMiddleware(
+            thunk,
        )
    );
    return store;
}
```

### userAction.js

thunkではactionに（非同期）処理を書くことになります。

```diff
export const updateName = name => (
    {
        type: 'UPDATE_NAME',
        name: name
    }
);

+export const getHelloByThunk = () => async dispatch => {
+    const response = await fetch('http://www.bluecode.jp/test/api.php');
+    const json = await response.json();

+    dispatch({
+        type: 'GET_HELLO_BY_THUNK',
+        name: json.message
+    });

}
```

### userReducer.js

取得した値でnameを更新するための記述をreducerに書きます。
thunkで更新されたとわかるよう、' by thunk'という文字列を追加しておきます。

```diff
const initialState = {
    user: {
        name: 'hoge',
        age: 33,
    }
}

const userReducer = (state = initialState, action) => {
    switch(action.type){
        case 'UPDATE_NAME':
            return Object.assign({}, state, {
                user: {
                    name: action.name
                }
            });
+        case 'GET_HELLO_BY_THUNK':
+            return Object.assign({}, state, {
+                user: {
+                    name: action.name + ' by thunk'
+                }
+            });
        default:
            return state;
    }
}

export default userReducer;
```

### Home.js

HomeにてAPIから値を処理できるようにします。
ここではボタンを追加し、ボタンを押したら取得した値でnameを更新するようにします。

```diff
import React from 'react';
import { View, Text, Button } from 'react-native';
import { connect } from 'react-redux';
+import { updateName, getHelloByThunk } from '../actions/userAction';

class Home extends React.Component {
    render() {
        return (
            <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
                <Text>Home</Text>
                <Button
                    title='Link to Detail'
                    onPress={() => this.props.navigation.navigate('Detail')}
                />
                <Text>{this.props.state.userData.user.name}</Text>
                <Button
                    title='updateName'
                    onPress={() => this.props.updateName('foo@Home')}
                />
+                <Button
+                    title='updateNameByThunk'
+                    onPress={() => this.props.getHelloByThunk('foo@Thunk')}
+                />
            </View>
        );
    }
}

const mapStateToProps = state => (
    {
        state: state,
    }
);

const mapDispatchToProps = dispatch => (
    {
        updateName: (name) => dispatch(updateName(name)),
+        getHelloByThunk: () => dispatch(getHelloByThunk()),
    }
);

export default connect(mapStateToProps, mapDispatchToProps)(Home);

// export default Home;
```

動きました。


## Sagaによる非同期処理

小規模なうちはactionに書いておけばいいのですが、大規模あるいはテストのしやすさを考えるとRedux-Saga
を利用することになります。同じ処理をSagaで行ってみます。

### モジュールのインストール

```bash
npm install --save redux-saga
```

### フォルダとファイルの準備

Sagaのベストプラクティスがわかっていませんが、とりあえず以下のようにします。

```bash
mkdir sagas
touch sagas/saga.js
```

### createStore.js

Sagaが利用できるよう記述を追加します。

```diff
import { createStore as reduxCreateStore, combineReducers, applyMiddleware } from 'redux';
import userReducer from './reducers/userReducer';
import thunk from 'redux-thunk';
+import createSagaMiddleware from 'redux-saga';
+import rootSaga from './sagas/saga';

export default createStore = () => {
    const sagaMiddleware = createSagaMiddleware();
    const store = reduxCreateStore(
        combineReducers({
            userData: userReducer,
        }),
        applyMiddleware(
            thunk,
+            sagaMiddleware,
        )
    );
+    sagaMiddleware.run(rootSaga);
    return store;
}
```

### saga.js

Sagaによる処理を記述します。
やっていることは単純ですが、どこから記述し始めるのかが難しいです。。。

```js
import { call, takeLatest, put, all, takeEvery } from 'redux-saga/effects';
import { updateName } from '../actions/userAction';

//callされる処理（普通の関数なのでテストしやすい）
const getHello = async () => {
    const response  = await fetch('http://www.bluecode.jp/test/api.php');
    const json = await response.json();
    return json;
}

//actionがマッチしたときの処理（2段階になっている）
function* requestHello(action){
    const json = yield call(getHello);
    yield put(updateName(json.message + ' by saga'));
}

//同系統の処理をまとめている
const helloSagas = [
    takeLatest('GET_HELLO_BY_SAGA', requestHello),
];

//たくさんの処理をまとめている
export default function* rootSaga(){
    yield all([
        ...helloSagas,
    ]);
}
```

### userAction

処理の起点となるactionを記述。

```diff
export const updateName = name => (
    {
        type: 'UPDATE_NAME',
        name: name
    }
);

export const getHelloByThunk = () => async dispatch => {
    const response = await fetch('http://www.bluecode.jp/test/api.php');
    const json = await response.json();

    dispatch({
        type: 'GET_HELLO_BY_THUNK',
        name: json.message
    });

}

+export const getHelloBySaga = () => (
+    {
+        type: 'GET_HELLO_BY_SAGA',
+    }
+);
```

### Home.js

Homeから呼び出せるよう処理を追加します。

```diff
import React from 'react';
import { View, Text, Button } from 'react-native';
import { connect } from 'react-redux';
+import { updateName, getHelloByThunk, getHelloBySaga } from '../actions/userAction';

class Home extends React.Component {
    render() {
        return (
            <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
                <Text>Home</Text>
                <Button
                    title='Link to Detail'
                    onPress={() => this.props.navigation.navigate('Detail')}
                />
                <Text>{this.props.state.userData.user.name}</Text>
                <Button
                    title='updateName'
                    onPress={() => this.props.updateName('foo@Home')}
                />
                <Button
                    title='updateNameByThunk'
                    onPress={() => this.props.getHelloByThunk('foo@Thunk')}
                />
+                <Button
+                    title='updateNameBySaga'
+                    onPress={() => this.props.getHelloBySaga('foo@Saga')}
+                />
            </View>
        );
    }
}

const mapStateToProps = state => (
    {
        state: state,
    }
);

const mapDispatchToProps = dispatch => (
    {
        updateName: (name) => dispatch(updateName(name)),
        getHelloByThunk: () => dispatch(getHelloByThunk()),
+        getHelloBySaga: () => dispatch(getHelloBySaga()),
    }
);

export default connect(mapStateToProps, mapDispatchToProps)(Home);

// export default Home;
```

動きました。