React-Redux简介和运用
容器组件/展示组件
Redux的React绑定库是基于 容器组件和展示组件分类 的开发思想。React-Redux将所有组件分成两大类: UI组件(presentational component)
和 容器组件(container component)
-
UI组件
- 只负责UI的呈现,不带有任何业务逻辑
- 没有状态
- 所有数据都由参数(this.props)提供
- 不使用任何 Redux的API
-
容器组件
- 负责管理数据和业务逻辑,不负责UI的呈现
- 带有内部状态
- 使用Redux 的API
UI组件负责UI的呈现,容器组件负责管理数据和逻辑 ,两者的关联由
connect
进行连接。
import { createStore } from "redux";
// 创建一个reducer,定义state中的修改规则
const countReducer = function(count = 0, action) {
switch(action.type) {
case 'ADD':
return count + 1;
case 'MINUS':
return count - 1;
default:
return count;
}
}
// 创建一个store来存储state
const store = createStore(countReducer);
export default store;
Provider
<Provider store>
使组件层级中的 connect()
方法都能够获得 Redux store
。正常情况下,你的根组件应该嵌套在 <Provider>
中才能使用 connect()
方法。
Provider
实际上就是Context
的<Context.Provider value={store}>
创建的组件。将Redux store
传入到子组件中。
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import ReactReduxPage from "./pages/ReactRedux/ReactReduxPage";
import store from './store';
ReactDOM.render(
<Provider store={store}>
<div className="center">
{/* <SetStatePage /> */}
{/* <ContextPage /> */}
{/* <HookPage /> */}
{/* <ReduxPage /> */}
<ReactReduxPage />
</div>
</Provider>,
document.getElementById("root")
);
connect()
connect([mapStateToProps], [mapDispatchToProps])(WrappedComponent)
[mapStateToProps(state, [ownProps]): stateProps]
(Function): 如果定义该参数,组件将会监听 Redux store 的变化。任何时候,只要 Redux store 发生改变,mapStateToProps 函数就会被调用。该回调函数必须返回一个纯对象[mapDispatchToProps(dispatch, [ownProps]): dispatchProps]
(Object or Function): 如果传递的是一个对象,那么每个定义在该对象的函数都将被当作Redux action creator
,对象所定义的方法名将作为属性名;每个方法将返回一个新的函数,函数中dispatch
方法会将action creato
r 的返回值作为参数执行。这些属性会被合并到组件的props
中
// UI组件
import { Component } from "react";
import { connect } from "react-redux";
import { mapStateToProps, mapDispatchToProps } from "./reactRedux";
class ReactReduxPage extends Component {
render() {
console.log(this.props);
return (
<div>
ReactReduxPage - store - {this.props.count}
<button onClick={this.props.addCount}>新增</button>
<br />
<button onClick={this.props.minusCount}>减少</button>
</div>
);
}
}
export default connect(mapStateToProps, mapDispatchToProps)(ReactReduxPage);
// 使用connect连接生成容器组件
// reactRedux.js
export const mapStateToProps = (state) => {
return {
count: state,
};
};
export const mapDispatchToProps = (dispatch) => {
return {
addCount: () => dispatch({ type: "ADD" }),
minusCount: () => dispatch({ type: "MINUS" }),
};
};
React-Redux源码实现
Provider
Provider
实际上是在根组件外面包了一层,将 store
放在上下文 context
中。这样所有的子组件都可以从 context
中拿到 store
// context.js
import React from 'react';
const Context = React.createContext();
export default Context;
// Provider.js
import Context from './context';
export default function Provider({ children, store }) {
// Provider的原理实际是Context属性
return <Context.Provider value={store}>{children}</Context.Provider>
}
connect
- 第一版
import { useContext } from 'react';
import { bindActionCreators } from '../Redux';
import Context from './context';
/**
* connect实际上是一个高阶组件,将store的数据挂载到wrappedComponent上
* @param {Function} mapStateToProps
* @param {Function | Object} mapDispatchToProps
* @returns
*/
const connect =
(mapStateToProps, mapDispatchToProps) => (WrappedComponent) => (props) => {
let stateProps = {},
dispatchProps = {};
const store = useContext(Context);
const dispatch = store.dispatch;
const subscribe = store.subscribe;
if(typeof mapStateToProps !== 'function') {
throw new Error('mapStateToProps必须是一个函数!');
}
if (typeof mapDispatchToProps !== 'object' && typeof mapDispatchToProps !== 'function') {
throw new Error('mapDispatchToProps必须为Function或者Object');
}
stateProps = mapStateToProps(store.getState());
// 函数中dispatch方法会将 action creator 的返回值作为参数执行,得到actionCreators
if (typeof mapDispatchToProps === 'function') {
dispatchProps = mapDispatchToProps(dispatch);
} else if (typeof mapDispatchToProps === 'object') {
dispatchProps = bindActionCreators(mapDispatchToProps, dispatch);
}
return <WrappedComponent {...props} {...stateProps} {...dispatchProps} />;
};
export default connect;
当前版本所存在的问题,点击按钮,store
中的state
s树会变化,但是视图不会重新渲染。
原因: store数据的变化无法触发视图更新,使用hooks
自定义一个触发视图更新的forceUpdate
。参考官方文档FAQ中的问题:有类似 forceUpdate 的东西吗?
// 优化后的connect.js
import { useContext, useEffect, useReducer } from "react";
import { bindActionCreators } from "../Redux";
import Context from "./context";
/**
* connect实际上是一个高阶组件,将store的数据挂载到wrappedComponent上
* @param {Function} mapStateToProps
* @param {Function | Object} mapDispatchToProps
* @returns 返回一个组件
*/
const connect =
(mapStateToProps, mapDispatchToProps) => (WrappedComponent) => (props) => {
let stateProps = {},
dispatchProps = {};
const store = useContext(Context);
const dispatch = store.dispatch;
const subscribe = store.subscribe;
if (typeof mapStateToProps !== "function") {
throw new Error("mapStateToProps必须是一个函数!");
}
if (
typeof mapDispatchToProps !== "object" &&
typeof mapDispatchToProps !== "function"
) {
throw new Error("mapDispatchToProps必须为Function或者Object");
}
stateProps = mapStateToProps(store.getState());
// 函数中dispatch方法会将 action creator 的返回值作为参数执行,得到actionCreators
if (typeof mapDispatchToProps === "function") {
dispatchProps = mapDispatchToProps(dispatch);
} else if (typeof mapDispatchToProps === "object") {
dispatchProps = bindActionCreators(mapDispatchToProps, dispatch);
}
const [ignore, forceUpdate] = useReducer((x) => x + 1, 0);
// 不使用useEffect: useEffect异步执行, 可能会导致丢失一些数据
useLayoutEffect(() => {
const unsubscribe = subscribe(() => {
forceUpdate();
});
// 返回一个清除函数
return () => {
// 视图卸载之后将订阅删除
if (unsubscribe) {
unsubscribe();
}
};
}, [subscribe]);
return <WrappedComponent {...props} {...stateProps} {...dispatchProps} />;
};
export default connect;
useStore
useStore
非常简单
// hooks.js
import Context from './Context';
function useStore() {
return useContext(Context);
}
useDispatch
useDispatch
也比较简单,在useStore
的基础上提取出dispatch
返回即可
// hooks.js
export function useDispatch() {
const { dispatch } = useStore();
return dispatch;
}
useSelector
- 初版
// hooks.js
export function useSelector(selector) {
const store = useStore();
const { getState } = store;
const selectedState = selector(getState());
return selectedState;
}
// function组件中的使用
import { useStore, useDispatch, useSelector } from "../../React-Redux";
export default function FunctionReactRedux() {
const store = useStore();
const dispatch = useDispatch();
console.log('store', store);
const handleAdd = () => {
dispatch({ type: 'ADD' })
}
const count = useSelector(state => state.count);
return <div>function - Children - {count} <button onClick={handleAdd}>增加</button></div>
}
初版的selector
与connect
初版存在同样的问题,无法触发视图的更新。使用同样的解决办法即可
- 优化版本
export function useSelector(selector) {
const store = useStore();
const { getState, subscribe } = store;
const selectedState = selector(getState());
const [ignore, forceUpdate] = useReducer(x => x+1, 0);
// 不使用useEffect: useEffect异步执行, 可能会导致丢失一些数据
useLayoutEffect(() => {
const unsubscribe = subscribe(() => {
forceUpdate(); // 实际为dispatch 触发视图更新
});
return () => {
// 视图卸载之后将订阅删除
if (unsubscribe) {
unsubscribe();
}
}
}, [subscribe]);
return selectedState;
}