React Native 应用中使用 Redux 管理状态

简介

本文假设你已经有一个可以运行的 React Native 应用,在此前提下介绍如何引入 Redux 管理状态。本文所使用的例子功能如下 :
1. 使用 Redux store 管理状态数据:一个用户名和一个计数器;
1. store对用户名可以有两种原子操作 : 设置为某个值,或者 清除用户名;
2. store对计数器可以有两种原子操作: 增加1,或者 减少1 ;
2. 使用一个 React Component 显示被管理的用户名和计数器, 如果用户名没有设置则只显示计数器;

本文通过以下步骤逐步实现以上功能的例子:

  • 引入 Redux 包
  • 创建 Redux reducer
  • 创建 Redux store
  • React 组件中使用 store 管理的状态 (直接使用,不通过 React Redux)
    • 获取 store 管理的当前状态
    • 通知 store 改变状态

然后简单地介绍Redux状态对象的组织结构。

本文并不打算深入探讨Redux全面的概念,也不打算探讨所涉及概念背后更深的原理。只给出一些能够帮你将Redux引入React Native应用中可以使用起来的必要知识。

引入 Redux 包

在你的React Native 目录下执行如下命令 :

npm install --save redux

完成后可以看到项目的package.json中 dependencies 区域新增如下一行:

    "redux": "^4.0.0",// 这里的版本号会根据你执行上面命令的时间的不同可能会不同

创建 Redux reducer

我们创建一个目录redux,然后在里面新增文件reducers.js,内容如下:

import {combineReducers} from 'redux'

/**
 * 管理用户名的 reducer, 所管理状态类型为基本数据类型:string
 * @param state 该reducer开始执行时的状态,初始/缺省为零长度字符串
 * @param action
 * @returns {string} 
 * 如果发生改动,返回新的状态对象,否则返回原状态对象
 */
function userReducer(state = "", action) {
    if (typeof action === 'undefined') {
        return state;
    }

    switch (action.type) {
        case 'SET_USERNAME':
            // 返回新的状态对象
            return action.userName;
        case 'CLEAR_USERNAME':
            // 返回新的状态对象
            return "";
        default:
            // 没有改动发生,返回原状态对象
            return state;
    }
}

/**
 * 管理计数器的 reducer, 所管理状态类型为对象数据结构: {value: number, updateTime: Date}
 * @param state 该reducer开始执行时的状态,初始/缺省为 {value: 0, updateTime: new Date()}
 * @param action
 * @returns {{value: number, updateTime: Date}} 
 * 如果发生改动,返回新的状态对象,否则返回原状态对象
 */
function counterReducer(state = {value: 0, updateTime: new Date()}, action) {
    if (typeof action === 'undefined') {
        return state;
    }

    const value = state.value;
    const now = new Date();
    switch (action.type) {
        case 'INCREMENT':
            // 构造新的状态对象并返回
            return {...state, value: value + 1, updateTime: now};
        case 'DECREMENT':
            // 构造新的状态对象并返回
            return {...state, value: value - 1, updateTime: now};
        default:
            // 没有改动发生,返回原状态对象
            return state;
    }
}

/**
 * 合并上面定义的各个 reducer 形成一个 reducer 用于创建 Redux store
 * @type {Reducer<any> | Reducer<any, AnyAction>}
 */
const combinedReducer = combineReducers({
    counter: counterReducer,
    user: userReducer,
});

export default combinedReducer;

在该文件中,我们创建了两个Redux reducer,然后将它们通过combineReducers组合成一个reducer用于最终创建store。Redux reducer扮演的角色是这样的,store接收到来自外部的指令action,通过它们响应这些action对状态做出改变。

创建 Redux store

现在我们已经创建了根据指令更新store中状态的reducer,现在我们来创建store。同样是在redux目录下,创建文件store.js,内容如下:

import {createStore} from 'redux'
// 引入我们刚才所创建的 reducer,也就是使用 combineReducers 组合后的那个 reducer
import combinedReducer from './reducers'

export default store = createStore(combinedReducer)

现在,我们已经拥有一个集中管理状态的Redux store实例了。根据Redux的建议,整个应用使用唯一的一个store管理状态。

React 组件中使用 store 管理的状态

本例子直接使用了Redux store的dispatch方法通知store执行指令action更新状态,也直接使用了Redux store的subscribe.getState方法获取store的最新状态。组件同时也通过store.subscribe监听了store的状态变化,这样一旦store里面的状态发生了变化,组件通过this.setState复制store的当前状态到组件自身从而触发页面的渲染。

实际上有一些工具可以用来简化React组件状态和Redux Store状态之间的同步,比如React Redux。但是为了更清楚直接地演示在React Native如何使用Redux这一目的,本例子没有引入React Redux,省得它影响我们的主要思路。

我们在redux目录平级的位置创建文件ReduxTest.js,它包含了一个React组件,演示使用上面所创建的Redux Store进行管理状态:

import React, {Component} from 'react';
import {StyleSheet, Text, TouchableOpacity, View} from 'react-native';
import store from './redux/store';


class ReduxTestScreen extends Component {
    constructor(props) {
        super(props);

        // 使用store中的状态初始化该组件的本地状态this.state
        const state = store.getState();
        // ??? 这里问自己一个问题 :       
        // 从store获取计数器属性为什么是 state.counter.value,而不是 state.counter,
        // state.value, 或者其他什么方式呢? Store 中的状态结构是如何组织的呢 ?
        this.state = {counter: state.counter.value, userName: state.user};

        // 准备一个监听器
        const changeListener = () => {
            // 当 store 中的状态发生变化时,
            // 将 store 中的新状态通过this.setState 设置到本组件,
            // 从而触发本组件被重新渲染输出
            console.log("in changeListener : ");
            console.log(store.getState());

            const state = store.getState();
            this.setState({counter: state.counter.value, userName: state.user});
        };
        // 监听Redux store中状态的变化
        store.subscribe(changeListener);
    }


    componentDidMount() {

    }

    /**
     * 向 store 发送dispatch 指令action,将状态变量用户名 userName 设置为 TOM
     * @private
     */
    _setUserNameTOM() {
        store.dispatch({type: 'SET_USERNAME', userName: 'TOM'});
    }

    /**
     * 向 store 发送dispatch 指令action,将状态变量用户名 userName 设置为 ''
     * @private
     */
    _clearUserName() {
        store.dispatch({type: 'CLEAR_USERNAME'});
    }

    /**
     * 向 store 发送dispatch 指令action,将状态变量计数器 counter 增加 1
     * @private
     */
    _increaseCounterBy1() {
        store.dispatch({type: 'INCREMENT'});
    }

    /**
     * 向 store 发送dispatch 指令action,将状态变量计数器 counter 减少 1
     * @private
     */
    _decreaseCounterBy1() {
        store.dispatch({type: 'DECREMENT'});
    }

    render() {
        const output = this.state.userName ? this.state.userName + ":" + this.state.counter : this.state.counter;
        return (
            <View style={styles.container}>
                <Text style={styles.outputText}>
                    {output}
                </Text>
                <View style={styles.buttonContainer}>
                    <TouchableOpacity onPress={this._setUserNameTOM} style={styles.button}>
                        <Text style={styles.buttonText}>
                            设置用户名
                        </Text>
                    </TouchableOpacity>
                    <TouchableOpacity onPress={this._clearUserName} style={styles.button}>
                        <Text style={styles.buttonText}>
                            清除用户名
                        </Text>
                    </TouchableOpacity>
                </View>
                <View style={styles.buttonContainer}>
                    <TouchableOpacity onPress={this._increaseCounterBy1} style={styles.button}>
                        <Text style={styles.buttonText}>
                            +1
                        </Text>
                    </TouchableOpacity>
                    <TouchableOpacity onPress={this._decreaseCounterBy1} style={styles.button}>
                        <Text style={styles.buttonText}>
                            -1
                        </Text>
                    </TouchableOpacity>
                </View>
            </View>
        );
    }
}

export default ReduxTestScreen;

const styles = StyleSheet.create({
    container: {
        flex: 1,
        backgroundColor: '#F5FCFF',
        margin: 0,
    },
    buttonContainer: {
        flex: 1,
        flexDirection: 'row',
        justifyContent: 'center',
        backgroundColor: 'transparent',
        margin: 0,
    },
    button: {
        height: 20,
        alignItems: 'center',
        justifyContent: `center`,
        backgroundColor: 'blue',
        padding: 4,
        margin: 8
    },
    outputText: {
        fontSize: 60,
        fontWeight: 'bold',
        textAlign: 'center',
        color: `#f00`
    },
    buttonText: {
        fontSize: 14,
        textAlign: 'center',
        color: `#fff`
    },
});

运行例子

初始界面

运行该例子,可以看到如下界面,此时store中状态变量用户名未设置,状态变量计数器为0。
初始界面

用户名/计数器变化的界面

用户名变化的界面
当点击按钮设置用户名后,用户名会输出在计数器前面,点击按钮清除用户名后,只会输出计数器。
当点击按钮+1或者-1时,你会看到界面上的计数器会相应地发生变化(此时用户名部分保持不变化)。
同时在控制台上,我们可以看到,每次store中的状态发生变化时,我们的监听器逻辑都会输出类似下面内容:

// +1 按钮点击导致的状态变更
ReactNativeJS: in changeListener:
ReactNativeJS: { counter:
ReactNativeJS:    { value: 1,
ReactNativeJS:      updateTime: Wed Apr 25 2018 16:42:24 GMT+0800 (CST) },
ReactNativeJS:   user: '' }
// 设置用户名 按钮点击导致的状态变更
ReactNativeJS: in changeListener:
ReactNativeJS: { counter:
ReactNativeJS:    { value: 1,
ReactNativeJS:      updateTime: Wed Apr 25 2018 16:42:24 GMT+0800 (CST) },
ReactNativeJS:   user: 'TOM' }

从上面的输出中,我们看到一个有意思的现象,整个例子并没有系统地说store中都会管理哪些状态,以及这些状态是以什么结构组织的,但是当我们试着输出store的状态时,它输出了一个包含了我们所预期的状态的对象。似乎有点神奇。那么,Redux store是如何组织我们的状态的呢 ?

理解Redux Store 中状态的组织方式

先给出一些结论性的东西,跟Redux Store中状态组织结构有关的要点有下面几个:
1. 每个reducer所操作的状态的结构
2. combineReducers合并每个reducer的方式

现在,我们结合本文的例子进行分析。

本文的例子中,我们创建了两个原子性的reducer,userReducercounterReduceruserReducer所操作的状态是一个字符串string,这是一个基本数据类型;而counterReducer所操作的状态是一个JavaScript对象,其结构为{value: number, updateTime: Date}。从上面运行时控制台的store.getState输出中,我们也可以看到这一点。

另外,看看本文的例子是如何合并reducer的:

const combinedReducer = combineReducers({
    counter: counterReducer,
    user: userReducer,
});

在这个合并中,参数是一个对象,counteruser是对象的两个属性,属性的值分别设置成了相应的reducer函数。再回顾一下上面控制台的输出,我们可以看到,实际的store.getState返回的结果正是这样结构一个对象,只不过输出中两个属性的值分别是对应的reducer所管理的状态对象的JSON。

由上不难看出,Redux store并没有从全局要求你设计整个状态的数据结构,或者说是组织方式,而是利用你合并reducer的方式和每个原子reducer管理状态的方式自动地帮你像搭积木一样构造了一个状态对象。明白了这一点,你才知道你想要的那个属性究竟该如何从store中拿到。

另外从本文的例子中你还可以看到另外一点,每个reducer中,状态发生变化时,返回的状态对象和变化前的状态对象是不同的,也就是说,store中对状态的更新不是发生在原状态对象上,而是基于变化构造出了一个新的状态对象。为什么要这样呢?这里其实牵涉到了另外一个概念:不可变性。有兴趣的朋友可以看看到底是为什么?

参考资料

官方文档中的例子

猜你喜欢

转载自blog.csdn.net/andy_zhang2007/article/details/80080780