Redux基础
Redux在任何地方都可用,不只是运用于React
redux用于state的管理,管理的是在整个网站各个地方都通用的state,但是不去管理那些只作用于组件自身的state
npm install redux -save
作用:将想要全局共享的公共数据(类似static数据)通过state进行保存,只能通过action进行修改,修改后影响全局
要点
- 项目中所有的state都以一个对象树的形式存储在一个单一的store中
- 只有触发action才能更改state
- 通过编写reducers来实现action更改state的逻辑
import { createStore } from 'redux'
// 创建一个store,createStore必须接受一个reducer,reducer为一个方法
let store = createStore(counter)
// 声明一个reducer,接收旧的state和action,根据action来制定修改state的逻辑代码并返回新的state
const state = 0
function counter(state, action) {
switch(action.type) {
case '+':
return state + 1
case '-':
return state - 1
default:
return state // 因为不是任何时候state都需要修改的,因此需要不改变state的出口
}
}
// action是一个普通的JS对象,普遍约定action内必须使用一个string的type字段来表示将要执行的动作
const add = {type: '-'}
const dec = {type: '+'}
// 改变内部state的唯一方法树dispatch一个action
store.dispatch(add) // -1
store.dispatch(dec) // 0
// 创建监听,一般用于更改state时进行渲染更新
store.subscribe()
store
store = createStore(reducer)
store.getState() // 获取当前state内容
store.dispatch(action) // 方法更新state
store.subscribe(listener) // 注册监听器,会返回一个可以解绑监听器的函数,执行该函数则会解绑
-
store.getState( )
获取所有传入reducer中的state的一个整合对象,该对象在每一次dispatch之后会更新
function A(state=0, action:any) {
switch(action.type) {
case("ADD"):
state += 1
return state
default:
return state
}
}
function B(state={state:0}) {
return state
}
let reducer = combineReducers({A, B}) // 最终state中显示的是什么属性名跟这里传进来的名字对应
let store = createStore(reducer)
store.getState() // { A:0, B:{state:0} }
store.dispatch({type:'ADD'})
store.getState() // { A:1, B:{state:0} }
-
store.dispatch(action)
触发state变化的唯一途径,会使用当前
getState()
和传入的action
以同步方式调用store的reduce函数,返回值会被作为下一个state,成为getState()
的新返回值,同时变化监听函数(change listener)被触发,当dispatch 时,会触发所有的 reducer 函数,会根据指定的 action 条件触发对应的 reducerdispatch()
方法的的参数必须是一个对象,如果是一个方法则会报错,因此action是一个创建函数,要store.dispatch(action())
//从发送action到redux内的state更新这一过程是同步的(即dispatch是同步操作),如果只是简单观察显现可能呈现出异步的效果,这是因为React的setState异步导致的
// 能确保执行完dispatch(A)只会才开始执行dispatch(B),不需要额外加上await
demo = anync () => {
dispatch(A)
await request.post(...) // 假设这里执行了一些异步操作
dispatch(B)
}
-
store.subscribe(listener)
每当执行
store.dispatch(action)
时便会自动执行此方法,需要操作当前state时可用store.getState()
获取当前state
action
action在声明reducer时并没有将具体的的action传入,真正将具体action传入reducer是在调用dispatch(action)的时候
使用action的两种方法:
- 直接使用一个对象
const action = {
type:''
}
- action创建函数,使用一个函数返回一个对象(约定传入参数放进payload属性里)
const action = (prams) => {
return {
type:''
payload: {
prams
}
}
}
reducers
通过触发store的dispatch方法,会触发reducer方法,reducer会拿到dispatch传入的action,再对action内的type属性值进行判断并采用对应方式更改state
这里触发的reducer实际上是 `store = createStore(reducer)` 中的reducer【使用combineReducers方法整合所有reducer】
而该reducer是所有reducer的集合
因此实际上当执行 `store.dispatch(atcion)` 时,会将action传入该集合,相当于触发了所有的reducer,然后执行满足条件的那一块语句
reducer函数拥有两个参数:初始state和传入的action,当触发 dispatch(action)
时,会将该action传递给reducer的第二个参数
const reducer = (initState, action) {
...
}
-
设计state结构
redux中所有state被保存在一个单一对象中,不同类型的state需要想办法进行区分
{
visibilityFilter: 'SHOW_ALL',
todos: [
{
text: 'Consider using Redux',
completed: true,
},
{
text: 'Keep all state in a single tree',
completed: false
}
]
}
-
保证reducer纯净
不可在reducer里执行如下操作:
- 修改传入参数
- 执行有副作用的参数,如API请求和路由跳转
- 调用非纯函数,如
Data.now()
或Math.random()
,这些函数每次调用返回的都是不同结果
只要传入参数相同,返回计算得到的下一个state就一定相同,没有特殊情况,没有副作用,没有API请求,没有变量修改,单纯执行计算
reducer的最终目的只是接受一个条件并根据条件修改state,其他不是以修改state为目的的操作应该都在外部操作,如在action中将拿到的数据处理完毕后再将最终结果传递给reducer执行
-
Action
redux首次执行时,state为
undefined
,可另设置初始state
function todoApp(state = initialState, action) {
// 这里暂不处理任何 action,仅返回传入的 state
return state
}
目录管理
方式一:统一管理reducer和action
因为每次 dispatch(action) 都会触发所有的 reducer,任意 action 也可以在任意一个组件的 dispatch 中使用,因此为了能方便看到所有的 action 和 reducer,可以统一写在一个文件里,同理 action 的 type 也可以全部写在一个文件里方便导入
目录结构
src
|
|__ actions
| |__ index.js
| |__ actionTypes.js
|
|__ reducers
| |__ index.js
|
|__ store.js
- 管理reducers
在项目src目录下创建reducers文件夹
,用于统一存放reducer文件,创建index.js文件,用于统一导入reducer文件并导出
import reducerA from './reducerA'
import reducerB from './reducerB'
import {combineReducers} from 'redux' // redux专门用来提供合并reducers的工具
export default combineReducers({
reducerA, // 相当于 reducerA: rerducerA
reducerB
})
/*
不可以直接导出,这样导出的不是一个方法,无法被识别
export default {
reducerA,
reducerB
}
*/
一开始进行reducer的编写时,只需写明state和声明一个简单方法导出即可,后续再加功能
const reducerA_state = {
// ...
}
export default (state = reducerA_state, action) => {
switch(action.type) {
default:
return state
}
}
-
管理store
在项目的src目录下创建
store.js
文件(注意只能存在一个store)
import {createStore} from 'redux'
import rootReducer from './reducers'
export default createStore(rootReducer)
-
管理actions
在项目src目录下创建
actions文件夹
,用于统一存放action文件,一般新建一个actionType.js
文件统一对action的type进行管理并导出,然后另创建具体的action.js文件使用action创建函数
的方式导出具体action如:实现一个商品计数器的action
// actionType.js
export default {
CART_AMOUNT_INCREMENT: 'CART_AMOUNT_INCREMENT',
CART_AMOUNT_DECREMENT: 'CART_AMOUNT_DECREMENT'
}
// /src/actions/cart.js
import actionType from './actionType'
export const increment = (id) =>{
return {
type: actionType.CART_AMOUNT_INCREMENT,
payload: {
id
}
}
}
export const decrement = (id) => {
return{
type: actionType.CART_AMOUNT_DECREMENT,
payload: {
id
}
}
}
在reducer里导入的是actionType.js,在使用store.dispatch( )方法的组件中导入cart.js
// /src/reducers/cart.js
import actionType from '../actions/actionType'
const initState = [{
id: 1,
title: 'Apple',
price: 8888.66,
amount: 10
},{
id: 2,
title: 'Orange',
price: 4444.66,
amount: 12
}]
export default (state = initState, action) => {
switch(action.type) {
case actionType.CART_AMOUNT_INCREMENT:
return state.map(item => {
if(item.id === action.payload.id) {
item.amount += 1
}
return item
})
default:
return state
}
}
import store from '../../store'
import {increment, decrement} from '../../actions/cart'
// ...
<button onClick = {
() => {
this.store.dispatch(decrement(id))
}
}/>
<button onClick = {
() => {
this.store.dispatch(increment(id))
}
}/>
方式二:按功能管理reducer和action
在 /src
下建立 store
目录,在该目录下新建一个 index.js
文件,用于创建 redux 和引入中间件
在 store
目录下根据功能创建各功能目录,在该目录内创建功能涉及到的 action 和 reducer 和 types
这样就能很明确的指定哪些功能有哪些 action 和 会触发哪些 reducer
不按组件划分的原因可能会有很多组件用到了同样的 reducer 和 action 功能,他们很多时候不只是服务于一个组件的,用组件的关系划分可能会存在很多重复
具体该如何分开这些功能则按具体项目要求决定,如一个小型网站只有用户和用户操作的功能
则可以分为两块,如 user 和 doing 文件
user文件:负责用户的登入注册登出等这些操作所需要的reducer和action功能
doing文件:负责用户的留言点赞转发等这些操作所需要的reducer和action功能
目录结构
src
|
|__ store
|
|__ user
| |__ actions.js
| |__ reducer.js
| |__ types.js
|
|__ index.js
recat-redux
npm install react-redux -s
react-redux是redux对react的官方绑定库,借助react-redux可以很方便的在react中使用redux
react-redux需要配合redux来使用,redux对外暴露了以下方法,除了这些都是react-redux的内容
export {
createStore,
combineReducers,
bindActionCreators,
applyMiddleware,
compose,
__DO_NOT_USE__ActionTypes
}
使用步骤:
-
通过createStore()方法创建store;
-
通过Provider组件将store注入到需要使用store的组件中;
-
通过connect()连接UI组件和容器组件,从而更新state和dispatch(action)
运用思想
实际项目中,需要权衡是直接使用Redux还是用React-Redux
-
React-Redux 将所有组件分成两大类:UI 组件(presentational component)和容器组件(container component)
-
UI 组件负责 UI 的呈现,容器组件负责管理数据和逻辑,如果一个组件既有 UI 又有业务逻辑,则将它拆分成下面的结构:外面是一个容器组件,里面包了一个UI 组件,前者负责与外部的通信,将数据传给后者,由后者渲染出视图
-
React-Redux 规定,所有的 UI 组件都由用户提供,容器组件则是由 React-Redux 自动生成,也就是说,用户负责视觉层,状态管理则是全部交给它
UI组件
- 只负责 UI 的呈现,不带有任何业务逻辑
- 没有状态(即不使用this.state这个变量)
- 所有数据都由参数(this.props)提供
- 不使用任何 Redux 的 API
容器组件
- 负责管理数据和业务逻辑,不负责 UI 的呈现
- 带有内部状态state
- 使用 Redux 的 API
UI组件 | 容器组件 | |
---|---|---|
作用 | 描述如何展现(骨架、样式) | 描述如何运行(数据获取、状态更新) |
直接使用 Redux | 否 | 是 |
数据来源 | props | 监听 Redux state |
数据修改 | 从 props 调用回调函数 | 向 Redux 派发 actions |
调用方式 | 手动 | 通常由 React Redux 生成 |
组件交流
Provider
使用provider进行快捷组件交流,不需要多层传递数据
使用时需要将其包在要传递组件的最外层,由于<App/>
为组件渲染根元素,因此任意子组件内都可直接使用provider传递的数据,无需层层相传,必须要要拥有store属性,值为创建的store
// /src/index.js
import React from 'react'
import { render } from 'react-dom'
import App from './App'
import store from './store'
import {Provider} from 'react-redux'
render(
<Provider store={store}>
<App/>,
</Provider>,
document.querySelector('#root')
)
connect
通过connect( )( )自动生成的容器组件(高阶组件),经过connect操作后会将dispatch方法传入该组件
使用了connect之后是自动订阅state的变化并进行重新渲染的,不需要再去通过store.subscribe(listener)
去监听state的变化
import {connect} from 'react-redux'
export default VisibleMyComponent = connect()(myComponent) // 一般合起来写,作用和下面的一样
/*
const VisibleMyComponent = connect()(myComponent)
export default VisibleMyComponent
使用装饰器的写法:
@connect() // connect()()有两层括号,使用装饰器后会少一个
class myComponent {...}
export default myComponent
*/
使用connect后会在原来传进组件props的基础上增加内容
class componentA {} // props为传递进来的内容
@connect()
class componentA {} // props为传递进来的内容和dispatch方法
connect( )接受两个参数:mapStateToProps
和 mapDispatchToProps
,它们定义了 UI 组件的业务逻辑,前者负责输入逻辑,即将state映射到 UI 组件的参数(props),后者负责输出逻辑,即将用户对 UI 组件的操作映射成 Action
它们主要是为了让redux使用更加方便而服务的,即使不设置也不影响其功能
const VisibleMyComponent = connect(
mapStateToProps,
mapDispatchToProps
)(myComponent)
-
mapStateToProps()
它是一个函数,返回一个对象,对象内的属性值(一般是state中的属性)会被加到组件的props中,为了更方便的获取到state值
当在connect绑定该函数时,对应的函数会自动传进来一个值,这个state为
store.getState()
的值在组件执行render方法前被执行,因此每次render后的组件状态都与store同步
// 若原组件的props内容为{a:1}
// 使用connect高阶组件化并设置mapStateToProps的组件props为{a:1, addPrpps:xxx, dispatch: function}
const mapStateProps = (state, ownProps) => { // ownProps为传进该组件的的props
return {
addProps:state.myState
}
}
- mapDispatchToProps()
它可以是一个函数,也可以是一个对象,相当于封装了一个传入固定action的dispatch方法并添加到组件的props,为了更方便地使用dispatch方法
在 connect()
传入该形参,则不会再将 dispatch
方法传入props,取而代之的是封装映射的方法
mapDispatchToProps()在组件constructor()中被执行,因而只执行一次
为一个函数时:当在connect绑定该函数会传入dispatch方法,然后手动封装一个触发固定action的dispatch函数,可以写死传入形参也可以不写入具体形参,具体使用时再传入
const increment = (id) =>{
return {
type: actionType.CART_AMOUNT_INCREMENT,
payload: {
id
}
}
}
const decrement = (id) => {
return{
type: actionType.CART_AMOUNT_DECREMENT,
payload: {
id
}
}
}
// 第一个参数用于接受store.dispatch()方法,第二个参数用于接受组件自身的props
const mapDispatchToProps = (dispatch, ownProps) => {
return {
add: (id) => { dispatch(increment(id)) },
dec: (id) => { dispatch(decrement(id)) }
}
}
// 封装后:
// 使用 this.props.dispatch(increment(id)) 等同于使用 this.props.add(id)
// 使用 this.props.dispatch(decrement(id)) 等同于使用 this.props.dec(id)
为一个对象时,键值内容应为action对象或action创建函数,会自动dispatch该对象中的所有属性(为创建函数时则是执行后的action结果)
const increment = (id) => {
return {
type: 'CART_AMOUNT_INCREMENT',
payload: {
id
}
}
}
const decrement = (id) => {
return {
type: 'CART_AMOUNT_DECREMENT',
payload: {
id
}
}
}
const mapDispatchToProps = {
add: increment,
add: decrement,
}
// this.props.add(id)
一般是在action先写好了mapStateToProps和mapDispatchToProps再导出到组件并传到connect里
// 为了展示方便全写在一个文件里,实际在项目中需要分类各个文件
import React, { Component } from 'react'
import ReactDOM from 'react-dom'
import { createStore } from 'redux'
import { Provider, connect } from 'react-redux'
// 定义counter组件
class Counter extends Component {
render() {
const { value, onIncreaseClick } = this.props
return (
<div>
<span>{value}</span>
<button onClick={onIncreaseClick}>自增按钮</button>
</div>
)
}
}
// Action
const increaseAction = { type: 'increase' }
// Reducer 基于原有state根据action得到新的state
function counter(state = { count: 0 }, action) {
switch (action.type) {
case 'increase':
return { count: state.count + 1 }
default:
return state
}
}
// 根据reducer函数通过createStore()创建store
const store = createStore(counter)
// 将state映射到Counter组件的props
function mapStateToProps(state) {
return {
value: state.count
}
}
// 将action映射到Counter组件的props
function mapDispatchToProps(dispatch) {
return {
onIncreaseClick: () => dispatch(increaseAction)
}
}
// 传入上面两个函数参数,将Counter组件变为App组件
const App = connect(
mapStateToProps,
mapDispatchToProps
)(Counter)
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)