Howto Make react-redux Work With react-navigation

2017/10/21

Tags: react-native redux react-redux react-navigation

这周花了一些时间研究 react-redux 和怎么让它和 react-navigation 配合一起工作,总结一下,把代码和注释直接贴这里了,也可以看这个 gist

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
/**
 * 一个简单的 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,
    },
});

Comments