Howto Make react-redux Work With react-navigation
#React-Native #Redux #React-Redux #React-Navigation
这周花了一些时间研究 react-redux
和怎么让它和 react-navigation
配合一起工作,总结一下,把代码和注释直接贴这里了,也可以看这个 gist。
1/**
2 * 一个简单的 RN 应用,有 2 个页面,使用了 react-navigation 的 StackNavigator 来做界面管理
3 * 为了说明如何使用 redux,以及如何让 redux 和 StackNavigator 配合
4 * 为了容易理解,把所有内容都放到了一个页面里面,实际开发的时候不要这么做
5 * 参考:
6 * https://github.com/jackielii/simplest-redux-example
7 * http://www.ruanyifeng.com/blog/2016/09/redux_tutorial_part_three_react-redux.html
8 */
9
10import React, { Component } from 'react';
11import {
12 StyleSheet,
13 Text,
14 View,
15 Button
16} from 'react-native';
17
18import { Provider, connect } from 'react-redux';
19import { createStore, combineReducers } from 'redux';
20import { StackNavigator, addNavigationHelpers } from 'react-navigation';
21
22// Home 页面,UI 组件
23class MyHome extends Component {
24 constructor(props) {
25 super(props);
26 console.log('init home, props', props);
27 }
28
29 _nextPage() {
30 // navigation 依然在 this.props 里面获取,和不用 redux 的时候用法一样
31 let {navigation} = this.props;
32 navigation.navigate("App");
33 }
34
35 render() {
36 // 所有的传递过来的状态,都需要从 this.props.screenProps 里面读取 (4)
37 // 我这里给不同页面的 action 取了各自的命名空间,避免冲突,也可以直接所有 action 都在一个命名空间,这块我还在摸索如何处理比较好 (5)
38 let {onIncButtonClicked} = this.props.screenProps.MyAppActions;
39
40 // 界面有两个按钮,一个用来增加另外一个页面的计数器,一个用来访问下一个页面
41 return (
42 <View style={styles.container}>
43 <Button title="Inc counter" onPress={onIncButtonClicked}></Button>
44 <Button title="Next page" onPress={()=>this._nextPage()}></Button>
45 </View>
46 )
47 }
48}
49
50// 这个组件只是用来测试就算一个 props 传递给子组件,在 props 被修改的时候也会被自动刷新
51class ShowText extends Component {
52 render() {
53 let {counter} = this.props;
54
55 return (
56 <Text>{counter}</Text>
57 )
58 }
59}
60
61// App 页面,UI 组件
62class MyApp extends Component {
63 constructor(props) {
64 super(props);
65 console.log('init App, props', props);
66 }
67
68 componentWillReceiveProps(newProps) {
69 console.log('myapp recive props', newProps);
70 }
71
72 render() {
73 // 组件的 state/props 获取,有自己的命名空间 (1)
74 let {counter} = this.props.screenProps.MyApp;
75 // 组件的 action props (5)
76 let {onIncButtonClicked, onDecButtonClicked} = this.props.screenProps.MyAppActions;
77
78 // 界面有一个计数器的结果,两个按钮
79 return (
80 <View style={styles.container}>
81 <ShowText counter={counter} />
82 <Button title="Inc counter" onPress={onIncButtonClicked}></Button>
83 <Button title="Dec counter" onPress={onDecButtonClicked}></Button>
84 </View>
85 )
86 }
87}
88
89// 初始化 StackNavigator,定义页面路由
90let AppNavigator = StackNavigator({
91 Home: {
92 screen: MyHome
93 },
94 App: {
95 screen: MyApp
96 }
97});
98
99// 包装一下 StackNavigator,因为有些参数需要定制一下
100class MyStackNavigator extends Component {
101 constructor(props) {
102 super(props);
103 console.log("inside MyStackNavigator", props);
104 }
105
106 render() {
107 // screenProps: 使用这个往所有的页面传递 props,这个是和直接使用 redux 不同的地方 (4)
108 // navigation: 因为使用 redux 之后,就不会直接操作 this.state 了,所以得告诉 StackNavigator dispatch 方法和 state 从哪里读取
109 return (
110 <AppNavigator
111 screenProps={this.props}
112 navigation={addNavigationHelpers({
113 dispatch: this.props.dispatch, // 通过 action props 定义 (2)
114 state: this.props.nav, // 通过 state props 定义 (3)
115 })} />
116 )
117 }
118}
119
120// 定义 state 和 props 的关系,所有 redux 应用都需要 (6)
121let mapStateToProps = (state, ownProps) => {
122 console.log("inside mapstate to props", state, ownProps);
123 return {
124 // 这两个是不同的命名空间,和上面你使用的时候的路径对应 (1)
125 "MyApp": state.MyApp,
126 "MyHome": state.MyHome,
127 // 定义 StackNavigator 的 state (3)
128 "nav": state.nav
129 }
130};
131
132// 定义 action 和 props 的关系,所有 redux 应用都需要
133let mapDispatchToProps = (dispatch, ownProps) => {
134 console.log("inside map dispath to props");
135 return {
136 // 这两个也是不同的命名空间,和上面使用的时候路径对应 (5)
137 'MyAppActions': {
138 onIncButtonClicked: () => {
139 let action = {
140 type: "INC_COUNTER",
141 payload: 1
142 };
143
144 dispatch(action);
145 },
146 onDecButtonClicked: () => {
147 let action = {
148 type: "DEC_COUNTER",
149 payload: -1
150 };
151
152 dispatch(action);
153 }
154 },
155 'MyHomeActions': {
156 onNextButtonClicked: () => {
157 let action = {
158 type: "NEXT_PAGE"
159 };
160
161 dispatch(action);
162 }
163 },
164 // 定义 StackNavigator 的 action props (2)
165 'dispatch': dispatch
166 }
167}
168
169// 定义 home 页面的 reducer,不过因为那个页面唯一的一个 action 是触发别的页面的动作的,所以这个 reducer 其实也可以没有
170// 所以从这里也能看出来,reducer 并不一定按照页面去分
171let homeReducer = (state, action) => {
172 console.log("inside home reducer", state, action);
173 return state || {};
174};
175
176// 定义一个初始化的 state
177let myAppInitState = { 'counter': 10};
178// 定义 app 页面的 reducer
179let myAppReducer = (state = myAppInitState, action) => {
180 // 收到的 state 实际上只是自己命名空间下的 (6)
181 console.log("inside myAppReducer", state, action);
182 let myState = state;
183 // 需要处理的 action 的逻辑
184 // 要注意,一个 action 被触发的时候,所有的 reducer 都会被调用,所以其实更像是订阅自己想要处理的 action
185 switch (action.type) {
186 case "DEC_COUNTER":
187 case "INC_COUNTER":
188 // 如果修改了 state,必须要返回一个新的对象,不能直接在原对象上修改,否则 state 变化不会触发组件的刷新
189 return Object.assign({}, myState, {
190 'counter': myState.counter + action.payload
191 });
192 default:
193 return state;
194 }
195};
196
197// 定义一个 StackNavigator 用到的初始化状态,这个很重要
198const initialState = AppNavigator.router.getStateForAction(AppNavigator.router.getActionForPathAndParams('Home'));
199// 定义 StackNavigator 的 reducer,代码直接复制来的
200const navReducer = (state = initialState, action) => {
201 console.log("inside nav reducer", state, action);
202 const nextState = AppNavigator.router.getStateForAction(action, state);
203
204 // Simply return the original `state` if `nextState` is null or undefined.
205 return nextState || state;
206};
207
208// 创建 store
209let store = createStore(combineReducers({
210 // 这里的 MyApp 等和前面定义 mapStateToProps 的地方对应 (6)
211 // 这里也是导致 reducer 收到的 state 只有自己命名空间下数据的一个原因 (6)
212 MyApp: myAppReducer,
213 MyHome: homeReducer,
214 nav: navReducer
215}));
216
217// 让 redux 加持一下,保佑
218let App = connect(mapStateToProps, mapDispatchToProps)(MyStackNavigator);
219
220// 其他的就是比较常见的 redux 的逻辑了,另外需要说明的是实际使用的时候,肯定会做页面拆分,如何拆分可能都会有不同的看法,我也还在摸索
221export default class Root extends Component<{}> {
222 constructor(props) {
223 super(props);
224 }
225
226 render() {
227 return (
228 <Provider store={store}>
229 <App prop1="prop1" />
230 </Provider>
231 );
232 }
233}
234
235
236const styles = StyleSheet.create({
237 container: {
238 flex: 1,
239 justifyContent: 'center',
240 alignItems: 'center',
241 backgroundColor: '#F5FCFF',
242 },
243 welcome: {
244 fontSize: 20,
245 textAlign: 'center',
246 margin: 10,
247 },
248 instructions: {
249 textAlign: 'center',
250 color: '#333333',
251 marginBottom: 5,
252 },
253});