応用編2の続きです。
シリーズ
- React-Navigatorを利用してみる(基礎編)
- RN応用編1:Drawer Navigatorをハンバーガーメニューで表示させる
- RN応用編2:TabやDrawerメニューにアイコンを設定する
- RN応用編3:Reduxで値の取り回し→この記事
概要
応用の本命です。
画面が増えてくると画面間での値や関数の共有が標準のpropsによる共有ではしんどくなってきます。
そこで値や関数を一言管理できるreduxを利用してみます。
また、Reduxが入れ子にしたページ等でも正しく機能するか確認します。
本当はReduxの利用が応用のメインですが、余計な記事(応用1、応用2)を書いてしまいました。
reduxを使うための準備
まずはReduxを利用するためのモジュールをインストールします。
reduxのインストール
reduxとreactで使うためのモジュールです。
npm install --save redux react-redux
いちおうデバッガも入れておきます。なお、デバッガのセットアップや利用についてはこちらの記事を参考にしてください。
デバッガのインストール
npm install --save-dev remote-redux-devtools
ファイルの整理(必須ではないのですが)
Reduxを利用を開始する前に記述量が多くなったApp.jsからStackやTabなど、画面制御(ルーティング)に関する部分をrouter.jsに切り出すことにします。
App.jsと同じ階層にrouter.jsを作成し、App.jsより以下の部分を切り出し、各constを外部から使えるようにexportしてやります。
router.js
切り出すだけなく、各定数を外部から利用できるようexportしておきます。
import React from 'react';
import { createStackNavigator, createBottomTabNavigator, createDrawerNavigator } from 'react-navigation';
import Icon from 'react-native-vector-icons/FontAwesome';
//import screens
import Single1 from './screens/Single1';
import Single2 from './screens/Single2';
import Stack1 from './screens/Stack1';
import Stack2 from './screens/Stack2';
import Tab1 from './screens/Tab1';
import Tab2 from './screens/Tab2';
+//stack
+export const Stack = createStackNavigator(
{
Stack1: { screen: Stack1 },
Stack2: { screen: Stack2 },
},
{
initialRouteName: 'Stack1'
}
);
+//Tab
+export const Tab = createBottomTabNavigator(
{
Tab1: {
screen: createStackNavigator({ Tab1: { screen: Tab1 } }),
navigationOptions: {
tabBarIcon: ({ tintColor }) => <Icon size={24} name="home" color={tintColor} />
}
},
Tab2: {
screen: createStackNavigator({ Tab2: { screen: Tab2 } }),
navigationOptions: {
tabBarIcon: ({ tintColor }) => <Icon size={24} name="cog" color={tintColor} />
}
},
}
);
+//drawer
+export const Drawer = createDrawerNavigator(
{
Stacks: {
screen: Stack,
navigationOptions: {
drawerIcon: <Icon name="check" size={24} />
}
},
Tabs: {
screen: Tab,
navigationOptions: {
drawerIcon: <Icon name="check" size={24} />
}
},
Single1: { screen: Single1 },
Single2: { screen: Single2 },
},
{
initialRouteName: 'Tabs'
}
);
App.js
router.jsに機能を切り出すことでApp.jsはDrawerを呼び出すだけのシンプルな内容になります。
import React from 'react';
import { createAppContainer } from 'react-navigation';
import { Stack, Tab, Drawer } from './router';
export default class App extends React.Component {
render() {
//AppContainerでラップ
const Layout = createAppContainer(Drawer);
return (
<Layout />
);
}
}
今後複雑な?プロセスが続くので、ここで一度、動作確認をしておきましょう。
機能は変更していないので切り出し前と同じ動きならOKです。
Reduxの利用のための最低限の実装と確認
Reduxの機構は慣れないと複雑なので、まず動作確認に必要な最低限の実装を行い、正しく動作しているか確認してみます。
ディレクトリファイルの追加
まずReduxを実装するためのディレクトリとファイルを追加しておきます。
まあ、ディレクリやファイルの配置の決定は宗教論争なところがありますが、ここでは以下にようにしたいと思います。
App.jsと同じ階層にactions, reducersディレクトリおよびcreateStore.jsファイルを作成。
さらにreducersの中にuserReducers.jsファイルを作成します。
コマンドだと以下の感じ。
mkdir actions reducers
touch createStore.js
cd reducers
touch userReducer.js
作成したファイルを順を追って利用していきます。
createStore.js
通常はreducderは複数になるかと思うので、reducerをまとめたりミドルウエアを定義してstoreを返すファイルを独立して作成します。
import { createStore as reduxCreateStore, combineReducers, compose, applyMiddleware } from 'redux';
import userReducer from './reducers/userReducer';
import composeWithDevTools from 'remote-redux-devtools';
export default function createStore() {
const store = reduxCreateStore(
combineReducers({
appData: userReducer,
}),
composeWithDevTools(applyMiddleware(
))
);
return store;
}
userReducer.js
user関連のデータを扱うreducerを個別に定義します。
また、初期値の値(データ形式)も決定しておきます。実装はstateをそのまま返す簡単なものです。
const initialState = {
user: {
name: 'tanaka',
age: 33
}
}
const userReducer = (state = initialState, action) => {
switch (action.type) {
default:
return state;
}
}
export default userReducer;
次にApp.jsを編集します。
storeを受け取り、<Provider>にpropsとして渡します。<Provider>で囲まれた範囲で共有が可能になります。
App.js
import React from 'react';
import { createAppContainer } from 'react-navigation';
+
+import { Provider } from 'react-redux';
+import createStore from './createStore';
import { Stack, Tab, Drawer } from './router';
+
+const store = createStore();
export default class App extends React.Component {
render() {
//AppContainerでラップ
const Layout = createAppContainer(Drawer);
return (
+ <Provider store={store}>
<Layout />
+
+ </Provider>
);
}
}
デバッガでの確認(ほんとに参考。無視していいです)
デバッガで確認するとuserReducer.jsで定義した初期値がstoreに保存されstateとして共有されているのが確認できます。
Reduxによる値(state)の共有
Reduxでは値(state)と関数(dispatch)を共有できますが、個別に見ていきましょう。
まずはstateから。
Stack1.js
初期値がすでにstoreに保持されてるはずなので初期ページ(Stack1)で表示させてみます。
表示させるためにはコンポーネントと共有の設定をconnect()で接続する必要があります。
connect()でコンテナ化する処理を別ファイルにわける場合もありますが、今回は1つのファイルに記述します。
stateをstate: stateと同階層でマッピングすると、JSX内でthis.props.stateとして参照可能になります。今回はnameを表示したいので、this.props.state.appData.user.nameで参照できるはずです(階層名と各種定義の関係を理解しましょう)。
stateはmapStateToPropsで定義したもの。appDataはcreateStoreで定義したもの。user.nameはreducerのinitialStateで定義したものです。通常は必要な情報を必要な階層だけ(小さく)マップします。
import React from 'react';
import { View, Text, Button } from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
+import { connect } from 'react-redux';
class Stack1 extends React.Component {
static navigationOptions = ({ navigation }) => ({
title: 'Stack1',
headerLeft: (
<Icon name="bars" size={24} onPress={() => { navigation.openDrawer() }} style={{ paddingLeft: 20 }} />
),
});
render() {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>Stack1</Text>
<Button
title='GoTo Stack2'
onPress={() => this.props.navigation.navigate('Stack2')}
/>
+
+ <Text>Reduxの値:{this.props.state.appData.user.name}</Text>
</View>
);
}
}
+const mapStateToProps = state => (
+ {
+ state: state,
+ }
+);
+export default connect(mapStateToProps, null)(Stack1);
### 動作確認
初期値で設定したtanakaが表示されています。
Reduxによる関数(dispatch)の共有
次に関数を共有してみましょう。nameをupdateするupdateName()関数を作成してみます。
actionsフォルダの中にuserAction.jsを作成し、以下の記述をします。
actionを定義し、それを返すaction create(関数)を定義しています。
actionの定義
export const updateName = name => {
return {
type: 'UPDATE_NAME',
name: name
}
}
reducerの定義(改修)
userReducerを編集します。
先程は最低限の実装しかしていませんでしたが、updateName()が呼び出された際の処理を追記します。
慣れないと???な感じですが大したことはしていません。
stateそのものは変更できないので、newStateというコピーを作り、そのコピーを編集した後、返しています(そうすることでstateが更新されます)。
const initialState = {
user: {
name: 'tanaka',
age: 33
}
}
const userReducer = (state = initialState, action) => {
+ switch(action.type){
+ case 'UPDATE_NAME':
+ const newState = Object.assign({}, state); //現在のstateをnewStateにコピー
+ newState.user.name = action.name; //nameを更新
+ return newState; //返す
default:
return state;
}
}
export default userReducer;
では初期ページ(Stack1)に関数を利用するボタンをついかしてみます。
ボタンを押すとnameがsuzukiに更新されます。どこのページで更新された値かわかるようにsuzuki@stack1としています。
Stack1.js
import React from 'react';
import { View, Text, Button } from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
import { connect } from 'react-redux';
+import { updateName } from '../actions/userAction';
class Stack1 extends React.Component {
static navigationOptions = ({ navigation }) => ({
title: 'Stack1',
headerLeft: (
<Icon name="bars" size={24} onPress={() => { navigation.openDrawer() }} style={{ paddingLeft: 20 }} />
),
});
render() {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Stack1</Text>
<Button
title='Goto Stack2'
onPress={() => this.props.navigation.navigate('Stack2')}
/>
<Text>Reduxの値:{this.props.state.appData.user.name}</Text>
+ <Button
+ title='Reduxによるnameのupdate'
+ onPress={() => this.props.updateName('suzuki@stack1')}
+ />
</View>
);
}
}
const mapStateToProps = state => (
{
state: state,
}
);
+const mapDispatchToProps = dispatch => {
+ return {
+ updateName: (name) => dispatch(updateName(name)),
+ }
+};
// export default Stack1;
+export default connect(mapStateToProps, mapDispatchToProps)(Stack1);
動作を確認してみます。
ボタンを押したらtanakaがsuzuki@stack1に変わりました。
各画面で共有できるか確認
1ページで値を共有するのであればReduxは必要ありません。
複数ページで値や関数が共有できるか試してみます。とりあえずTab2.jsにしこんでみます。
nameを表示させつつ、yamada@tab2という内容でnameを更新するボタンをつけました。
Tab2.js
import React from 'react';
import { View, Text, Button } from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
+import { connect } from 'react-redux';
+import { updateName } from '../actions/userAction';
class Tab2 extends React.Component {
render() {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Tab2</Text>
+ <Text>Reduxの値:{this.props.state.appData.user.name}</Text>
+ <Button
+ title='Reduxによるnameのupdate'
+ onPress={() => this.props.updateName('yamada@tab2')}
+ />
</View>
);
}
}
+const mapStateToProps = state => (
+ {
+ state: state,
+ }
+);
+const mapDispatchToProps = dispatch => {
+ return {
+ updateName: (name) => dispatch(updateName(name)),
+ }
+};
// export default Tab2;
+export default connect(mapStateToProps, mapDispatchToProps)(Tab2);
動作確認
Stack1で値を更新
まずStack1でnameを更新してみます。
Tab1で値の反映確認と値の更新
その状態でTab1に遷移してみると、nameがsuzuki@stack1になっています。
Tab2でnameを更新してみましょう。nameがyamada@tab2に変更されました。
Stack1で値の確認
再びStack1に戻ってみるとnameがyamada@tab2になっています。
各ページにおいて変数や関数が共有されていることが確認できました。
とりあえず以上です。