这比较适合看过redux但看了跟没看一样的同学,帮助记忆理清里面的基础知识
Store是保存数据的地方,可以把它看做一个容器。整个应用只能有一个Store。
Redux提供createStore
这个函数生成store
import { createStore } from 'redux';
const store = createStore(fn);
// store也可以接受第二个参数:state的初始值,他会覆盖reducer的state参数中的默认初始值
const store = createStore(fn, initState);
当你想拿到某个时刻的数据state时,需要生成对应的store快照。
const state = store.getState();
由于用户触碰不到代码(State),只能通过触碰View来通知代码服务(store)改变State,这个由View发起的通知就是Action。
Action是一个对象,其中type
是必须的,表示Action的名称。其他属性可以自由设置。
const action = {
type: "ADD",
payload: "data"
}
可以理解为,要触发一个ADD
行为,携带的参数是payload
。
类似于工厂模式,Action肯定会有很多种消息,因此可以写一个Action Creator来生产一个Action
function AddCreator(text) {
return {
type: "ADD",
payload: text
}
}
你可以将action理解为一个通知单,那么View如何发出这个通知单给代码服务(store)呢,就是通过dispatch
。
只要给dispatch传入一个action参数,View就把通知单发出去了!
import { createStore } from 'redux';
const store = createStore(fn);
const action = {
type: "ADD",
payload: "data"
}
store.dispatch(action);
View把通知单传给代码服务后,代码服务(Store)总要根据action中的内容去进行相应的处理的。reducer就是用来识别action并进行相应处理的处理器。他接收当前State和Action作为参数,返回一个新的State。
//其实就是先去判断action要执行哪个操作,再把对应的state更新并返回
function reducer = (state = initState, action) => {
switch(action.type) {
case 'ADD':
return state + action.payload;
default:
return state
}
}
注意这个处理器是store的,也就是说,store的创建是伴随reducer参数来初始化的。
import { createStore } from 'redux';
const store = createStore(reducer);
Store也提供了subscribe方法来设置监听函数,一旦State发生变化,就会自动执行这个函数。
store.subscribe
返回一个函数,调用这个函数可以解除监听。
const listener = function() {
console.log(store.getState())
}
let unsubscribe = store.subscribe(listener);
// 解除监听
unsubscribe();
你知道的,毕竟store不是react组件中的state,store中state的更新并不会让View也同时变化,你需要手动调用render重新渲染。
const Counter = ({ value, onIncrement, onDecrement }) => (
<div>
<h1>{value}</h1>
<button onClick={onIncrement}>+</button>
<button onClick={onDecrement}>-</button>
</div>
);
const reducer = (state = 0, action) => {
switch (action.type) {
case 'INCREMENT': return state + 1;
case 'DECREMENT': return state - 1;
default: return state;
}
};
const store = createStore(reducer);
const render = () => {
ReactDOM.render(
<Counter
value={store.getState()}
onIncrement={() => store.dispatch({type: 'INCREMENT'})}
onDecrement={() => store.dispatch({type: 'DECREMENT'})}
/>,
document.getElementById('root')
);
};
render();
// 通过监听state的改变来更新视图
store.subscribe(render);
当我们想要在store的整个流程中添加一些其他功能时,我们该往哪里添加?
处理流程中涉及逻辑进程的主要就是dispatch和reducer了,但reducer要求是纯函数,不能涉及副作用,那只能在dispatch中做手脚了。
先看一下想添加一个日志功能,并将更新后的state打印出来该如何实现:
let next = store.dispatch;
store.dispatch = function changeDispatch(action) {
console.log('dispatching', action);
next(action);
console.log('next state', store.getState());
}
上面对store.dispatch
进行了改造,添加了一些功能,这就是中间件的雏形。中间件其实就是一个函数。
Redux提供了原生方法applyMiddlewares
来加入中间件,增强store.dispatch
的功能。
他可以将中间件组成一个数组,然后依次执行。
import { applyMiddleware, createStore } from 'redux';
import createLogger from 'redux-logger';
const logger = createLogger();
const store = createStore(
reducer,
initial_state,
applyMiddleware(logger, ...otherMiddlewares)
);
Action发出后,过一段时间再执行Reducer,这就是异步。
那么Reducer作为纯函数肯定是不可以在这里添加处理异步逻辑的,那么就得在store.dispatch中做手脚了。
想一想,如果不能同步的进行dispatch(异步) -> reducer
,那不如将dispatch分为两步走,dispatch(异步获取) -> dispatch(同步发送action) -> reducer
。
// 操作发起时的 Action
// 操作成功时的 Action
// 操作失败时的 Action
具体如何改造多次的dispatch可以使用redux-thunk
或者redux-promise
,他们处理后,dispatch可以接受不同的参数,不再局限于action对象。
回忆下之前的Redux,state更新时我们需要设置监听函数实时re-render才能更新View,想要在其他组件中使用store都得引入一下。
React-Redux解决了上述的痛点:
React-Redux提供了组件,使得整个app能够访问到store中的数据,Provider是用context封装的。
React-Redux将组件分为两种,一种是UI组件,另一种是容器组件。
UI组件只通过接受props来显示UI,容器组件负责管理数据和业务逻辑(也就是承接state和dispatch)
React-Redux提供一个connect方法能够把组件和store连接起来,把state、dispatch方法都放入到当前组件上,同时是state变成响应式。其实就是一个高阶组件包装了一下我们写的UI组件。
// index.js
import React from 'react';
import ReactDom from "react-dom"
//引入Provider
import {Provider} from "react-redux";
import Counter from "./counter/ReactReduxCounter";
import store from "./redux/basic/store"
ReactDom.render(
<Provider store={store}>
<Counter></Counter>
</Provider>,
document.getElementById("root")
)
我们看到通过用<Provider/>
来包装我们真正的reactDOM,就可以让里面所有的子组件都使用store了。
注意:用到store的子组件应该是通过connect包装后的。
// Counter.js
import React, { Component } from 'react';
import {connect} from "react-redux"
//子组件
class Counter extends Component {
//方法调用后自动更新数据
increment=()=>{
this.props.increment()
}
decrement=()=>{
this.props.decrement()
}
render() {
return (
<div>
<div>{this.props.num}</div>
<button onClick={this.increment}>点我+1</button>
<button onClick={this.decrement}>点我-1</button>
</div>
);
}
}
//该函数作为connect的第一个参数,能拿到state
//映射state到组建的props上
function mapStateToProps(state){
return {
num:state.counter
}
}
//该函数作为connect的第二个参数,能拿到dispatch
//映射dispatch方法到组建的props上
function mapDispatchToProps(dispatch){
return {
increment(){
dispatch({
type:"increment"
})
},
decrement(){
dispatch({
type:"decrement"
})
}
}
}
//connet函数执行返回一个高阶组件
//调用这个高阶组件,传入当前组件Counter作为参数,返回一个增强的Counter组件
//这个增强的组件props里有store的state和dispach方法
export default connect( mapStateToProps , mapDispatchToProps )(Counter)
/// store.js
import { createStore } from "redux"
const defaultState={
counter:0
}
//纯函数
let reducers =(state = defaultState ,action)=>{
switch (action.type){
case "increment":
console.log("increment")
return {
counter:state.counter+1
}
case "decrement":
return {
counter:state.counter-1
}
default :
return state
}
}
const store = createStore(reducers)
export default store
注意到,上述connect()中传递了两个参数:mapStateToProps()和mapDispatchToProps()。
通过观察connect源码(下一节)发现,他是利用了context
封装了一个高阶组件,在封装过程中,会调用上述两个方法,把store中的state和dispatch取出来放到子组件的props里。这样我们在编写组件的时候,就可以无感知的调用state和dispatch了。
mapStateToProps
他是一个函数,接受state作为参数,返回一个对象,这个对象是你想从state中取出的数据
const mapStateToProps = (state) => {
return {
counter: state.counter
}
}
maoDispatchToProps
他是一个函数,接受dispatch和ownProps(容器组件的props)两个对象,返回一个对象,这个对象中的值应该是一个包含可以触发dispatch的方法。
const mapDispatchToProps = (dispatch, ownProps) => {
return {
increment(num) {
dispatch({
type: "increment",
payload: num
})
}
}
}
function connect(mapStateToProps, mapDispatchToProps) {
return function(WrappedComponent) {
class Connect extends React.Component {
componentDidMount() {
// 组件加载完成后订阅store变化,如果store有变化则更新UI
this.unsubscribe = this.context.store.subscribe(this.handleStoreChange.bind(this));
}
componentWillUnmount() {
// 组件销毁后,取消订阅事件
this.unsubscribe();
}
handleStoreChange() {
// 更新UI
this.forceUpdate();
}
render() {
return (
<WrappedComponent
{...this.props}
{...mapStateToProps(this.context.store.getState())} // 参数是store里面的数据
{...mapDispatchToProps(this.context.store.dispatch)} // 参数是store.dispatch
/>
);
}
}
Connect.contextTypes = {
store: PropTypes.object
};
return Connect;
};
}
可能我们觉得每次都要用connect封装一下组件太麻烦了,还要写mapStateToProps和mapDispatchToProps,react-redux为我们提供了一些hooks,比如常用的useSelector和useDispatch。
import { useSelector, useDispatch } from 'react-redux'
const Home = (props) => {
const dispatch = useDispatch();
const number = useSelector(state => state.number);
const handleClick = () => {
dispatch({
type: 'ADD',
payload: 1
})
}
return (
<h2>{number}</h2>
<button onClick={handleClick}/>
)
}
依初初初学者短见,我们想要在React项目中使用共享数据时,使用的store都是通过Redux提供的createStore创建的store。React-Redux只是帮我们封装了一些方法,以帮助我们在子组件中更便利的使用state和dispatch等。
如果说React-Redux是在Redux的基础上帮助子组件更便捷的使用state和dispatch。
那Rematch则是在和React-Redux搭配基础上帮我们更具封装性的创建store,加上更便利的使用state和dispatch。
之前我们是通过redux的createStore的方法来创建store。
import { createStore } from 'redux';
const store = createStore(reducer, initState);
如果还想要添加一些异步处理,那可能还要加入中间件,reducer也变得很复杂。
而Rematch提供了init可以让我们把initState、reducers、异步处理方法封装在一起。
// store.js
import { init } from '@rematch/core';
const count = {
state: 0,
reducers: {
increment: (state, payload) => state + payload,
decrement: (state, payload) => state - payload
},
effects: { // 带有异步处理数据的reducer
async incrementAsync(payload) {
await func();
this.increment(payload);
}
}
}
export const store = init(count);
如果到此为止,你可以用上述生成的store正常的配合react-redux的provider、connect去使用他。
rematch建议我们将数据分类保存到models文件夹下的不同的文件里。最后在models/index.js中统一导出。
例如:
// models/index.js
export { default as count } from './count'
export { default as user } from './user'
// store.js
import { init } from '@rematch/core'
import * as models from './models'
export const store = init({models})
于是我们在每个组件上,可以直接引入store来取到想要的某个模块中的store和dispatch
// counter.js
import { store } from './store.js'
const counter = () => {
const { counter, user } = store.getState();
const add = store.dispatch.counter.add;
const changName = store.dispatch.user.changeName;
const handleClick = () => {
add({
type: 'ADD',
payload: 1
})
}
return (
<h2>{counter.number}</h2>
<h2>{user.name}</h2>
<button onClick={handleClick}/>
)
}
但注意,以上state不是响应式的,也就是说触发dispatch改变state并不会让视图更新,我们依然需要结合react-redux中的connect或者hook来让其变成响应式。