在上一章中,我们已经学习了 redux 内部的原理,今天我们就更进一步,来看看作为 react 特化的 react-redux。
同样,先来看看应用。
react-redux 主要有两类 api
- 用于提供数据:Provider
- 用于接收数据:connect,hooks
其中 connect 是作为 react 16.8 以前的主要手段,而 hooks(useSelector、useDispatch)则是之后新推出的 api。
下面我们分别来看看如何应用吧:
// index.js
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import store from './store'
import {Provider} from "react-redux";
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
// ReactReduxPage.js
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
function mapStateToProps(state) {
return {
count: state.count,
}
}
function mapDispatchToProps(dispatch) {
let creators = {
add: () => ({ type: 'ADD', payload: 1 }),
}
creators = bindActionCreators(creators, dispatch)
return { dispatch, ...creators }
}
@connect(mapStateToProps, mapDispatchToProps)
class ReactReduxPage extends Component {
render() {
const { count, add } = this.props
return (
<>
<div>React Redux Page</div>
<p>{count}</p>
<button onClick={add}>add</button>
</>
)
}
}
export default ReactReduxPage
这里需要注意的是,mapDispatchToProps 还可以传入一个 object:
{
add: () => ({type: "ADD", payload: 1}),
}
这样和作为 function 的区别在于,object 无法在 props 中直接获取 dispatch。
在 16.8 之后,官方更加推荐使用 hooks 的写法:
import React, { useCallback } from 'react'
import { useSelector, useDispatch } from 'react-redux'
export default function ReactReduxHooksPage() {
const count = useSelector((state) => state.count)
const dispatch = useDispatch()
const add = useCallback(() => {
dispatch({
type: 'ADD',
payload: 1,
})
}, [])
return (
<div>
<h3>React Redux Hooks Page</h3>
<p>{count}</p>
<button onClick={add}>add</button>
</div>
)
}
可以看到,与纯 redux 相比,在 react-redux 中,我们不必再手动的监听,更新视图,对于数据的获取也变得更加方便简洁,那么接下来我们就来看看,react-redux 是怎么实现的吧。
Provider
看到 Provider 的使用方法以及功能,第一时间想到的应该是 react 的 context:
import React, { createContext } from 'react'
const Context = createContext()
export const Provider = ({ store, children }) => {
return <Context.Provider value={store}>{children}</Context.Provider>
}
接下来我们替换 Provider,并且通过 console 查看一下是否能够成功获取到 store:
看样子是没有问题的。
connect
数据提供没有问题,那么我们来到数据获取。
首先 connect 接收两个(实际上在 react-redux 中是 4 个)可选参数,并且在执行之后能够修饰 react 组件,那么它的架子还是可以比较轻松的搭出来的:
const connect = (mapStateToProps, mapDisPatchToProps) => (WrappedComponent) => (props) => {
return <WrappedComponent {...props} />
}
我们先来处理 state,思路很简单:
mapStateToProps 是一个函数,在执行之后获取对应的 state。那么我们可以先通过 context 获取 store,然后将 getState 传给 mapStateToProps,最后将 state 传给 WrappedComponent 即可。
import React, { useContext } from 'react'
import { Context } from './Provider'
const getStateProps = (mapStateToProps) => (getState) => {
return mapStateToProps(getState())
}
export const connect = (mapStateToProps, mapDisPatchToProps) => (WrappedComponent) => (props) => {
const store = useContext(Context)
const { getState } = store
const stateProps = getStateProps(mapStateToProps)(getState)
return <WrappedComponent {...props} {...stateProps} />
}
接下来是 dispatch,思路与上面是类似的,需要注意的是,这里要区分一下 mapDisPatchToProps 的类型:
const getDispatchProps = (mapDisPatchToProps) => (dispatch) => {
let dispatchProps = {
dispatch,
}
if (typeof mapDisPatchToProps === 'object') {
dispatchProps = bindActionCreators(mapDisPatchToProps, dispatch)
} else if (typeof mapDisPatchToProps === 'function') {
dispatchProps = mapDisPatchToProps(dispatch)
}
return dispatchProps
}
export const connect = (mapStateToProps, mapDisPatchToProps) => (WrappedComponent) => (props) => {
const store = useContext(Context)
const { getState, dispatch } = store
const stateProps = getStateProps(mapStateToProps)(getState)
const dispatchProps = getDispatchProps(mapDisPatchToProps)(dispatch)
return <WrappedComponent {...props} {...stateProps} {...dispatchProps} />
}
最后,我们需要在 state 改变的时候重新 render,具体技术再 redux 篇中已经介绍过了,这里就不再赘述:
export const connect = (mapStateToProps, mapDisPatchToProps) => (WrappedComponent) => (props) => {
const store = useContext(Context)
const [, forceUpdate] = useReducer((x) => x + 1, 0)
const { getState, dispatch, subscribe } = store
const stateProps = getStateProps(mapStateToProps)(getState)
const dispatchProps = getDispatchProps(mapDisPatchToProps)(dispatch)
useEffect(() => {
const unSubscribe = subscribe(() => {
forceUpdate()
})
return () => {
unSubscribe && unSubscribe()
}
}, [])
return <WrappedComponent {...props} {...stateProps} {...dispatchProps} />
}
最终效果如下:
hooks
hooks 的 api 有两个,分别是 useSelector 和 useDispatch,使用上面已经简单介绍过了,这里来看看实现。
useSelector
这个 hook 接收一个查询函数,并且返回对应的 state,思路如下:
- 获取 store
- 获取 getState
- 查询 state
- 返回 state
直接按照这个思路编写代码即可:
import { useContext } from 'react'
import { Context } from './Provider'
const useStore = () => {
return useContext(Context)
}
export const useSelector = (selector) => {
const store = useStore()
const { getState } = store
const state = selector(getState())
return state
}
useDispatch
同上,思路非常简单:
- 获取 store
- 获取 dispatch
- 返回 dispatch
export const useDispatch = () => {
const store = useStore()
return store.dispatch
}
最后就是更新,那么更新是放在 useSelector 还是 useDispatch 中呢?
显然,状态修改的时候才需要更新,而很多获取 dispatch 的操作可能并没有修改状态,那么更新的位置也就很明显了:
export const useSelector = (selector) => {
const [, forceUpdate] = useReducer((x) => x + 1, 0)
const store = useStore()
const { getState } = store
useEffect(() => {
const unSubscribe = subscribe(() => {
forceUpdate()
})
return () => {
unSubscribe && unSubscribe()
}
}, [])
const state = selector(getState())
return state
}
拓展
到这里,一个简单的 react-redux 实际上已经实现了,但还有一点美中不足的地方:
我们知道 useEffect 是执行是会延后的,那么假如在组件渲染完成之后,到 useEffect 执行之间发生了状态的改变,显然是不会触发 render 的,那么这里我们可以借助 react 提供的另一个 api:useLayoutEffect。