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;
動きました。