LoginSignup
9
7

More than 3 years have passed since last update.

ReactNative(個人用)作業テンプレート

Last updated at Posted at 2018-11-10

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上にあります(ソースコードは更新されていません)。
また、各STEPに対応したtagが設定してあります。目的に応じてtagを指定してcloneしてもいいでしょう。

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

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

準備

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

npm install -g expo

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

expo init react-temp
cd react-temp

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

とりあえず表示

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

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

mkdir screens
touch screens/Home.js

Home.js

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

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を読み込み表示します。

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で。

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

npm install --save react-navigation
npm install

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

ファイルの準備

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

touch screens/Detail.js

Detail.js

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

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に各ファイルを設定します。

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に移動するボタンを設置します(戻るボタンは自動生成されます)。

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の利用は避けられません。

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

npm install --save redux react-redux

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

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

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

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

createStore.js

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

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するので、その部分も記述しています。

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を設定します。

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において初期値が取得できるか試します。

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においても同様に初期値が取得できるか試します。

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をアップデートするメソッドを実装してみます。

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

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

mkdir actions
touch actions/userAction.js

userAction.js

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

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

userReducer.js

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

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で利用してみます。

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を利用してみます。

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

npm install --save redux-thunk

createStore.js

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

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に(非同期)処理を書くことになります。

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'という文字列を追加しておきます。

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を更新するようにします。

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で行ってみます。

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

npm install --save redux-saga

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

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

mkdir sagas
touch sagas/saga.js

createStore.js

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

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による処理を記述します。
やっていることは単純ですが、どこから記述し始めるのかが難しいです。。。

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を記述。

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から呼び出せるよう処理を追加します。

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;

動きました。

9
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
7