Howto Make react-redux Work With react-navigation
这周花了一些时间研究 react-redux
和怎么让它和 react-navigation
配合一起工作,总结一下,把代码和注释直接贴这里了,也可以看这个 gist。
/**
* 一个简单的 RN 应用,有 2 个页面,使用了 react-navigation 的 StackNavigator 来做界面管理
* 为了说明如何使用 redux,以及如何让 redux 和 StackNavigator 配合
* 为了容易理解,把所有内容都放到了一个页面里面,实际开发的时候不要这么做
* 参考:
* https://github.com/jackielii/simplest-redux-example
* http://www.ruanyifeng.com/blog/2016/09/redux_tutorial_part_three_react-redux.html
*/
import React, { Component } from 'react';
import {
StyleSheet,
Text,
View,
Button
} from 'react-native';
import { Provider, connect } from 'react-redux';
import { createStore, combineReducers } from 'redux';
import { StackNavigator, addNavigationHelpers } from 'react-navigation';
// Home 页面,UI 组件
class MyHome extends Component {
constructor(props) {
super(props);
console.log('init home, props', props);
}
_nextPage() {
// navigation 依然在 this.props 里面获取,和不用 redux 的时候用法一样
let {navigation} = this.props;
navigation.navigate("App");
}
render() {
// 所有的传递过来的状态,都需要从 this.props.screenProps 里面读取 (4)
// 我这里给不同页面的 action 取了各自的命名空间,避免冲突,也可以直接所有 action 都在一个命名空间,这块我还在摸索如何处理比较好 (5)
let {onIncButtonClicked} = this.props.screenProps.MyAppActions;
// 界面有两个按钮,一个用来增加另外一个页面的计数器,一个用来访问下一个页面
return (
<View style={styles.container}>
<Button title="Inc counter" onPress={onIncButtonClicked}></Button>
<Button title="Next page" onPress={()=>this._nextPage()}></Button>
</View>
)
}
}
// 这个组件只是用来测试就算一个 props 传递给子组件,在 props 被修改的时候也会被自动刷新
class ShowText extends Component {
render() {
let {counter} = this.props;
return (
<Text>{counter}</Text>
)
}
}
// App 页面,UI 组件
class MyApp extends Component {
constructor(props) {
super(props);
console.log('init App, props', props);
}
componentWillReceiveProps(newProps) {
console.log('myapp recive props', newProps);
}
render() {
// 组件的 state/props 获取,有自己的命名空间 (1)
let {counter} = this.props.screenProps.MyApp;
// 组件的 action props (5)
let {onIncButtonClicked, onDecButtonClicked} = this.props.screenProps.MyAppActions;
// 界面有一个计数器的结果,两个按钮
return (
<View style={styles.container}>
<ShowText counter={counter} />
<Button title="Inc counter" onPress={onIncButtonClicked}></Button>
<Button title="Dec counter" onPress={onDecButtonClicked}></Button>
</View>
)
}
}
// 初始化 StackNavigator,定义页面路由
let AppNavigator = StackNavigator({
Home: {
screen: MyHome
},
App: {
screen: MyApp
}
});
// 包装一下 StackNavigator,因为有些参数需要定制一下
class MyStackNavigator extends Component {
constructor(props) {
super(props);
console.log("inside MyStackNavigator", props);
}
render() {
// screenProps: 使用这个往所有的页面传递 props,这个是和直接使用 redux 不同的地方 (4)
// navigation: 因为使用 redux 之后,就不会直接操作 this.state 了,所以得告诉 StackNavigator dispatch 方法和 state 从哪里读取
return (
<AppNavigator
screenProps={this.props}
navigation={addNavigationHelpers({
dispatch: this.props.dispatch, // 通过 action props 定义 (2)
state: this.props.nav, // 通过 state props 定义 (3)
})} />
)
}
}
// 定义 state 和 props 的关系,所有 redux 应用都需要 (6)
let mapStateToProps = (state, ownProps) => {
console.log("inside mapstate to props", state, ownProps);
return {
// 这两个是不同的命名空间,和上面你使用的时候的路径对应 (1)
"MyApp": state.MyApp,
"MyHome": state.MyHome,
// 定义 StackNavigator 的 state (3)
"nav": state.nav
}
};
// 定义 action 和 props 的关系,所有 redux 应用都需要
let mapDispatchToProps = (dispatch, ownProps) => {
console.log("inside map dispath to props");
return {
// 这两个也是不同的命名空间,和上面使用的时候路径对应 (5)
'MyAppActions': {
onIncButtonClicked: () => {
let action = {
type: "INC_COUNTER",
payload: 1
};
dispatch(action);
},
onDecButtonClicked: () => {
let action = {
type: "DEC_COUNTER",
payload: -1
};
dispatch(action);
}
},
'MyHomeActions': {
onNextButtonClicked: () => {
let action = {
type: "NEXT_PAGE"
};
dispatch(action);
}
},
// 定义 StackNavigator 的 action props (2)
'dispatch': dispatch
}
}
// 定义 home 页面的 reducer,不过因为那个页面唯一的一个 action 是触发别的页面的动作的,所以这个 reducer 其实也可以没有
// 所以从这里也能看出来,reducer 并不一定按照页面去分
let homeReducer = (state, action) => {
console.log("inside home reducer", state, action);
return state || {};
};
// 定义一个初始化的 state
let myAppInitState = { 'counter': 10};
// 定义 app 页面的 reducer
let myAppReducer = (state = myAppInitState, action) => {
// 收到的 state 实际上只是自己命名空间下的 (6)
console.log("inside myAppReducer", state, action);
let myState = state;
// 需要处理的 action 的逻辑
// 要注意,一个 action 被触发的时候,所有的 reducer 都会被调用,所以其实更像是订阅自己想要处理的 action
switch (action.type) {
case "DEC_COUNTER":
case "INC_COUNTER":
// 如果修改了 state,必须要返回一个新的对象,不能直接在原对象上修改,否则 state 变化不会触发组件的刷新
return Object.assign({}, myState, {
'counter': myState.counter + action.payload
});
default:
return state;
}
};
// 定义一个 StackNavigator 用到的初始化状态,这个很重要
const initialState = AppNavigator.router.getStateForAction(AppNavigator.router.getActionForPathAndParams('Home'));
// 定义 StackNavigator 的 reducer,代码直接复制来的
const navReducer = (state = initialState, action) => {
console.log("inside nav reducer", state, action);
const nextState = AppNavigator.router.getStateForAction(action, state);
// Simply return the original `state` if `nextState` is null or undefined.
return nextState || state;
};
// 创建 store
let store = createStore(combineReducers({
// 这里的 MyApp 等和前面定义 mapStateToProps 的地方对应 (6)
// 这里也是导致 reducer 收到的 state 只有自己命名空间下数据的一个原因 (6)
MyApp: myAppReducer,
MyHome: homeReducer,
nav: navReducer
}));
// 让 redux 加持一下,保佑
let App = connect(mapStateToProps, mapDispatchToProps)(MyStackNavigator);
// 其他的就是比较常见的 redux 的逻辑了,另外需要说明的是实际使用的时候,肯定会做页面拆分,如何拆分可能都会有不同的看法,我也还在摸索
export default class Root extends Component<{}> {
constructor(props) {
super(props);
}
render() {
return (
<Provider store={store}>
<App prop1="prop1" />
</Provider>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
welcome: {
fontSize: 20,
textAlign: 'center',
margin: 10,
},
instructions: {
textAlign: 'center',
color: '#333333',
marginBottom: 5,
},
});