简介
本文假设你已经有一个可以运行的 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,userReducer
和counterReducer
。userReducer
所操作的状态是一个字符串string
,这是一个基本数据类型;而counterReducer
所操作的状态是一个JavaScript对象,其结构为{value: number, updateTime: Date}
。从上面运行时控制台的store.getState
输出中,我们也可以看到这一点。
另外,看看本文的例子是如何合并reducer的:
const combinedReducer = combineReducers({
counter: counterReducer,
user: userReducer,
});
在这个合并中,参数是一个对象,counter
和user
是对象的两个属性,属性的值分别设置成了相应的reducer函数。再回顾一下上面控制台的输出,我们可以看到,实际的store.getState
返回的结果正是这样结构一个对象,只不过输出中两个属性的值分别是对应的reducer所管理的状态对象的JSON。
由上不难看出,Redux store并没有从全局要求你设计整个状态的数据结构,或者说是组织方式,而是利用你合并reducer的方式和每个原子reducer管理状态的方式自动地帮你像搭积木一样构造了一个状态对象。明白了这一点,你才知道你想要的那个属性究竟该如何从store中拿到。
另外从本文的例子中你还可以看到另外一点,每个reducer中,状态发生变化时,返回的状态对象和变化前的状态对象是不同的,也就是说,store中对状态的更新不是发生在原状态对象上,而是基于变化构造出了一个新的状态对象。为什么要这样呢?这里其实牵涉到了另外一个概念:不可变性。有兴趣的朋友可以看看到底是为什么?