React组件化(以Ant-Design为例)
组件化编程,只需要去安装好对应的组件,然后通过各式各样的组件引入,实现快速开发
我们这里学习的是 Ant-design
(应该是这样),它有很多的组件供我们使用
引入各种组件,可以方便开发,省着自己去二次封装组件,同时也更好看了
快速起步
安装antd组件库
npm install antd
随便一个组件里面引入
import { Button } from "antd";class App extends React.Component {render() {<div><Flex gap="small" wrap="wrap"><Button type="primary">Primary Button</Button><Button>Default Button</Button><Button type="dashed">Dashed Button</Button><Button type="text">Text Button</Button><Button type="link">Link Button</Button></Flex></div>}
}
export default App;
运行一下
大概就是这么个东西,一定学会看官方文档,尤其这种组件类的,不看文档就啥也做不了
AntDesign官方文档
其他UI,但是是国外的
material-ui
大概就是这些东西,还有自定义主题的一些方法,因为每个版本的官方修改方式都不一样,所以建议现用现查
Redux
非必须学习的项目,但是如果项目里用了就得学
Redux简介
1.redux是一个专门用于做状态管理的JS库(不是react插件库,只是名字像)。
2.它可以用在react, angular, vue等项目中, 但基本与react配合使用。
3.作用: 集中式管理react应用中多个组件共享的状态。
也类似于VueX
什么时候需要用Redux
首先,Redux是在有很多很多组件的情况下,才有可能需要用到Redux的,如果是单个组件,就自然没有很复杂的传值需求了
Redux
适用于多交互、多数据源的场景。简单理解就是复杂
从组件角度去考虑的话,当我们有以下的应用场景时,我们可以尝试采用 Redux
来实现
- 某个组件的状态需要共享时
- 一个组件需要改变其他组件的状态时
- 一个组件需要改变全局的状态时
除此之外,还有很多情况都需要使用 Redux 来实现(还没有学 hook,或许还有更好的方法)
Redux工作流程
- store
store
是 Redux 的核心,可以理解为是 Redux 的数据流向指挥,我们可以将任何我们想要存放的数据放在 store
中,在我们需要使用这些数据时,我们可以从中取出相应的数据。因此我们需要先创建一个 store
,在 Redux 中可以使用 createStore
API 来创建一个 store
store是无法直接进行操作的,需要借助Redux进行操作
store是一个调度者,store是指挥者,不干活。需要任何操作,或者动作,都会去分发走,找到对应的担当来做。(如果越过了store,action直接去找reducers,就有点类似去餐厅点餐,不找前台点餐,直奔后厨要吃的😂)
- action
action
是 store
中唯一的数据来源,一般来说,我们会通过调用 store.dispatch
将 action 分发到 store
我们需要传递的 action
是一个对象,它必须要有一个 type
值(如果是初始化,就传@@init@@
代表要初始化),data值在第一次传参的时候可以为undefined,然后由Reducers来初始化赋值。
- reducers
在 Reducers 中,我们需要指定状态的操作类型(type),要做怎样的数据更新,因此这个类型是必要的。
因为Reducers中会有很多具体处理的Reducer,所以这里Reducers代表很多处理Reducer的集合。
reducer 会根据 action 的指示,对 state 进行对应的操作,然后返回操作后的 state
另外,Reducer可以加工状态,加工的前提是有上一次的状态值,如果没有状态值就要初始化一个状态值。有了状态值之后就可以进行下一步的加工。
手写一个Redux精简版
这里只是一个简化,忽略了很多东西(比如Creators创建Action的过程),侧重于展示store,reducer之间的关系
store.js文件:
创建一个store的js,导出供外界使用
import { createStore } from "redux";
//引入为store服务的Reducer
import countReducer from "./count_reducer";
//手动创建一个store
const store = createStore(countReducer);//全局只暴露这一个store对象
export default store;
count_reducer.js文件:
我们创建专门计数的reducer(简单来说就是专门负责处理某件事的function),这里取名叫作count_reducer
所有的action通过dispatch
传进去之后,类型,数据都托管于store这个中心,调用的组件只需要把 type,data
传进去,剩下的只需要等着从store中获取结果即可!
/*作为一个reducer应该有如下功能Store传来(previousState,action),Reducer接收到参数做出判断,previousState判断是否是空,是空就得初始化。非空就按照action进行下一步操作action处理完之后,把处理好的newState(previousState处理之后的版本),返回给store,等待Store返回给React组件所以以上的这些操作,只能用function。所以Reducers里的Reducer本质就是函数动作完成后,将数据暂存给store,等待后续组件获取值即可
*/export default function countReducer(previousState, action) {// 从Action对象里传来的action对象,里面包含type和data,所以我们需要解构出来。等待后续处理const { type, data } = action;if (previousState === undefined) {// 如果之前的状态是空的,就初始化之后还给store中心// 或者可以这么写,给参数给默认值// function countReducer(previousState=0, action) {return 0;}// 判断传入功能类型switch (type) {case "add"://这里用原来的值+新传入的值,得到新值return previousState + data * 1;case "sub"://这里用原来的值-新传入的值,得到新值return previousState - data * 1;// 提一嘴,这里都是return,所以不用breakdefault:return previousState;}
}
Count.jsx文件:
调用方组件:
import React, { Component } from "react";
// 获取store,用于获取store中的状态
import store from "../redux/store";export default class Count extends Component {// 之前准备的state,存储值count 都不用存在了,因为已经托管给store了// 但是,自己组件内部的值其实是还需要放在state的,因为store只需要托管复杂共享的情况state = {// 这个实际上就不要了,因为保存在state中// countNumber: "",// 但是比如car这个属性,只有自己用,就没必要兜一圈放redux中了,放自己组件里就挺好car: "威朗Pro GS",};add = () => {const { value } = this.count;// store是分发中心,告诉reducer要干的事情,以及传入的数值// 我们只需要将 type(要做的事情),value(原始值) 告诉store 。让store去找对应的reducer操作即可// 对应的reducer拿到数值,做出判断以及相应动作处理数据,处理好之后return值在store中等待组件get// 但注意,仅仅调用dispatch是不够的,因为redux是第三方js。无法触发页面刷新// 所以需要检测Redux里状态改变时,就去调用render。这里用到了订阅subscribe// 你都不用自己再解构接收值,所有的函数处理,值存储,都在redux中做好了,只需要我们从store中get结果即可// 获取store的地方,因为被监听所以自动刷新了,触发renderstore.dispatch({ type: "add", data: value });store.subscribe(() => {// 只要Redux里状态改变时,就去调用render。这里用到了订阅subscribe。手动触发一下就行// 借助setState,传个空值进去就可以触发render重新渲染// 当然,我们也可以在index.js的根标签监听这个。监听整个App组件,利用diffing算法全部更新,避免性能下降this.setState({});});};sub = () => {const { value } = this.count;store.dispatch({ type: "sub", data: value });store.subscribe(() => {// 手动监听,触发页面重新渲染this.setState({});});};render() {console.log(store);return (<div>当前结果<h1>{store.getState()}</h1>选择内容<select ref={(c) => (this.count = c)}><option value={1}>1</option><option value={2}>2</option><option value={3}>3</option></select><button onClick={this.add}>+</button><button onClick={this.sub}>-</button></div>);}
}
总结一下精简案例
(1).去除Count组件自身的状态
(2).src下建立:-redux-store.js-count_reducer.js(3).store.js:1).引入redux中的createStore函数,创建一个store2).createStore调用时要传入一个为其服务的reducer3).记得暴露store对象(4).count_reducer.js:1).reducer的本质是一个函数,接收:preState,action,返回加工后的状态2).reducer有两个作用:初始化状态,加工状态3).reducer被第一次调用时,是store自动触发的,传递的preState是undefined,传递的action是:{type:'@@REDUX/INIT_a.2.b.4}( _a.2.b.4是随机数,为了避免和自己写的type名有重合)(5).在index.js中监测store中状态的改变,一旦发生改变重新渲染<App/>备注:redux只负责管理状态,至于状态的改变驱动着页面的展示,要靠我们自己写。
完整版Redux
上面的案例没有写action的创建,所以这里补齐就成为了完整版。
该文件专门为Count组件需要用的Reducer去生成action对象,封装好type,这样在调用组件传一个data进来就可以了
新增文件:1.count_action.js 专门用于创建action对象2.constant.js 放置容易写错的type值
store.js文件:
创建一个store的js,导出供外界使用。这个和上面的没区别,这里不赘述了
count_action.js文件:
/*该文件专门为Count需要用的Reducer去生成action对象,封装好type,这样在调用组件传一个data进来就可以了
*/
import { ADD } from "./constant.js";// 注意,这里有个坑,就是我希望在这里返回对象的箭头函数,最外侧不能是花括号
// (data) => { type: ADD, data }; 如果是这样,会把 type: ADD, data 这部分识别为函数体,就没法返回对象了
// 所以我们用括号给括起来,就自动返回对象了
export const createAddAction = (data) => ({ type: ADD, data });
// 这两种写法等价
export function createIncrementAction(data) {// 要返回Action对象(返回type和data数据)return { type: ADD, data };
}
count_reducer.js文件:
本案例中,主要是引入常量来表示Type,避免了容易拼错的问题
import { ADD, SUB } from "./constant.js";export default function countReducer(previousState, action) {// 从Action对象里传来的action对象,里面包含type和data,所以我们需要解构出来。等待后续处理const { type, data } = action;if (previousState === undefined) {// 如果之前的状态是空的,就初始化之后还给store中心// 或者可以这么写,给参数给默认值// function countReducer(previousState=0, action) {return 0;}// 判断传入功能类型switch (type) {case ADD://这里用原来的值+新传入的值,得到新值return previousState + data * 1;case SUB://这里用原来的值-新传入的值,得到新值return previousState - data * 1;// 提一嘴,这里都是return,所以不用breakdefault:return previousState;}
}
Count.jsx文件:
调用方组件,这里主要强调变化的部分,省略了部分代码:
import React, { Component } from "react";
// 传入action函数,我们只需要传入data即可
import { createAddAction } from "../redux/count_action";
// 获取store,用于获取store中的状态
import store from "../redux/store";export default class Count extends Component {...省略add = () => {const { value } = this.count;// 引入了action后,就不需要我们手动定义type了,在action中已经做好定义了,我们只需要传入data即可// store.dispatch({ type: "add", data: value });// 引入,并且直接用count_action所封装好的函数来传入参数store.dispatch(createAddAction(value * 1));store.subscribe(() => {// 监听,store值变化重新renderthis.setState({});});};sub = () => {省略...};render() {return (省略...);}
}
异步action版
action有两种类型,根据返回值类型来区分
- action的值为Object,则为同步action(store可以直接接收处理)
//返回一个对象
export const createSubAction = (data) => ({ type: SUB, data });
- action的值为function,则为异步action(只有function才能开启异步任务,并且,store不可以直接接收处理异步action,需要通过中间件处理后才能接收)
export const createAddAsync = (data,time) => {return ()=>{setTimeout(() => {...省略}, time);}
};
之前的异步add方法,其本质还是在组件里写的,可以看到,等待的异步过程没有在Redux里面写
addAsync = () => {const { value } = this.count;setTimeout(() => {store.dispatch(createAddAction(value * 1));}, 5000);
};
我们现在要把异步等待的操作,放在Redux的Action Creators里面,不放在组件里面等待了
调用方组件Count:
addAsync = () => {const { value } = this.count;//常规调用,看似没问题store.dispatch(createAddAsync(value, 5000));store.subscribe(() => {// 手动监听,触发页面重新渲染(也可以去监听App组件)this.setState({});});
};
action组件:
export const createAddAsync = (data, time) => {return () => {// 这里其实只套了一个定时操作setTimeout(() => {// 通过store调用已经定义好的增加action,省着我们再写了store.dispatch(createAddAction(data));}, time);};
};
但实际上这是有问题的,运行代码会报错
翻译过来就是,store不直接接收action的值为function(异步action)
。想要接收必须去用一个中间件,让store允许接收函数
引入中间件:npm install redux-thunk
需要我们安装一下
引入完成之后,就需要在store.js
里面修改一下,用于支持异步action。做的这一切,只是为了让store可以接收异步action返回的函数
store.js
:
//引入store创建,以及中间件
import { createStore, applyMiddleware } from "redux";
//引入为store服务的Reducer
import countReducer from "./count_reducer";
// 引入thunk给applyMiddleware中间件用
import thunk from "redux-thunk";
//手动创建一个store,传入applyMiddleware(thunk)
const store = createStore(countReducer, applyMiddleware(thunk));//全局只暴露这一个store对象
export default store;
此时我们再看修改后的组件:
调用方组件Count(没有变化):
addAsync = () => {const { value } = this.count;store.dispatch(createAddAsync(value, 5000));store.subscribe(() => {// 手动监听,触发页面重新渲染(也可以去监听App组件)this.setState({});});
};
action组件:
// 异步action,就是只action的值为函数,在异步action中,一般都会调用同步action,异步action不是必须要用的
export const createAddAsync = (data, time) => {return () => {// 这里其实只套了一个定时操作setTimeout(() => {// 调用已经定义好的增加actionstore.dispatch(createAddAction(data));console.log(data, time);}, time);};// 不用store调用dispatch也可以,因为dispatch会自动传进来一个,这两种完全等价// return (dispatch) => {// // 这里其实只套了一个定时操作// setTimeout(() => {// // 调用已经定义好的增加action// dispatch(createAddAction(data));// console.log(data, time);// }, time);// };
};
此时再测试,不再报错。
配置完毕之后的store
如果我们给store传入一个普通类型的Object action,store就会直接找Reducer去做处理
如果给store传入一个异步类型的Function action,这个函数store就会帮你调用
总结下来,虽然异步的action调用的时候返回值是函数,但是最后一般都会调用同步action,来完成数据的操作
React18版本的store监听刷新
之前给的store subscribe监听刷新,是React17版本的,React18版本的可以参考这个
ReactDOM.render is no longer supported in React 18.
改造之后的index.js
// 引入React核心库
import React from "react";
import { createRoot } from "react-dom/client";
// 引入App标签
import App from "./App";
import store from "./redux/store";// React18版本监听并刷新页面
const root = createRoot(document.getElementById("root"));
root.render(<App />);//监测redux中状态的改变,如redux的状态发生了改变,那么重新渲染App组件
store.subscribe(() => {root.render(<App />);
});
总结异步action
(1).明确:延迟的动作不想交给组件自身,想交给action(2).何时需要异步action:想要对状态进行操作,但是具体的数据靠异步任务返回。(3).具体编码:1).npm install redux-thunk,并配置在store中2).创建action的函数不再返回一般对象,而是一个函数,该函数中写异步任务。3).异步任务有结果后,分发一个同步的action去真正操作数据。(4).备注:异步action不是必须要写的,完全可以自己等待异步任务的结果了再去分发同步action。
React-Redux基础
安装:npm install react-redux
安装不上去就用这个:npm install react-redux --legacy-peer-deps
还不行就把package.json里面的这俩都删了,在安装React-Redux
"redux": "^4.2.1",
"redux-thunk": "^2.4.2",
引言
React-Redux是React专门出的Redux,属于官方出品
宗旨在于将UI与redux分隔开,所有的操作都要经由容器组件
引言-简化版连接UI组件与容器组件
前置知识
redux里的唯一的api:connect
;
使用的时候,一般都是这么用connect()();
简单来说,得分开看connect()是一个返回一个函数的方法
connect()()是在connect()返回函数后,继续再次调用这个返回的函数
就有点类似下面这个,调用connect()()
,最后会触发a里的输出OK
当然,a()也可以返回一个返回值
connect(){return a();
}a(){console.log("OK")
}
首先明确文件应该放在哪个包下面:
UI组件:components包下
容器组件:containers包下
且UI组件的里面,不能有任何关于Redux相关的API(store,actionCreator,dispatch… 这些API都不能引入了)。只能有UI组件以及UI组件动作相关的东西。在components里创建countTemplate.jsx
countTemplate.jsx
:
import React, { Component } from "react";export default class CountTemplate extends Component {// 纯UI组件+页面操作//加法increment = () => {// 这个是获取页面的选择值const { value } = this.selectNumber;};...//异步加incrementAsync = () => {const { value } = this.selectNumber;};render() {//console.log('UI组件接收到的props是',this.props);return (<div><h1>当前求和为:{"???"}</h1><select ref={(c) => (this.selectNumber = c)}><option value="1">1</option><option value="2">2</option><option value="3">3</option></select> <button onClick={this.increment}>+</button> <button onClick={this.decrement}>-</button> <button onClick={this.incrementIfOdd}>当前求和为奇数再加</button> <button onClick={this.incrementAsync}>异步加</button> </div>);}
}
简化版里面,容器组件更像是一个桥梁,负责连接 UI组件与Redux 的交互。所以在Container里面,创建一个count.jsx
在这个桥梁(count.jsx
)里,我们可以导入UI组件等待连接,(理论上应该导入Redux的Store,就完成了,但是不行,store必须从App里面通过props传进来 )
我在container容器里面引入了store仍然报找不到的错(这里是错误示范)
//引入Count的UI组件
import CountTemplate from "../components/CountTemplate";
// 引入store
import { store } from "../../redux/store";
// 引入connect的API
import { connect } from "reat-redux";// 导入connect,并且连接UI
export default connect()(CountTemplate);
我已经引进来store了,但是还是提示找不到store,这里不是因为我没有在connect里连接,而是不允许这种用法。只能在调用Container组件的组件里传值用props进去。
注意:react-redux不允许直接引入,只能从Container组件被调用的那一级组件里传进来
App组件(调用Container的组件)。这样就不报错了
import React from "react";
import Count from "./pages/Count";
import "./App.css";
import store from "./redux/store";
class App extends React.Component {render() {return (<div>{/* 只能用props传递store进入Container组件 */}<Count store={store}></Count></div>);}
}
export default App;
省略store.subscribe
首先就是之前的store.subscribe,之前我们在Redux里面,由于组件和Redux之间没有直接监听更新的手段。所以这里需要手动去监听渲染组件
// 引入React核心库
import React from "react";
import { createRoot } from "react-dom/client";
// 引入App标签
import App from "./App";
import store from "./redux/store";// React18版本监听并刷新页面
const root = createRoot(document.getElementById("root"));
root.render(<App />);//监测redux中状态的改变,如redux的状态发生了改变,那么重新渲染App组件
store.subscribe(() => {root.render(<App />);
});
而在React-Redux中,connect就自动集成了监听并更新的功能,所以我们不必再手动监听。删掉即可,测试完美替换,不影响功能。
// 引入React核心库
import React from "react";
import { createRoot } from "react-dom/client";
// 引入App标签
import App from "./App";// React18版本监听并刷新页面
const root = createRoot(document.getElementById("root"));
root.render(<App />);
React-Redux的基本使用
上面说到,Container想接受到store,只能通过props形式传递,但是Container本身,并不是标签形式的,所以就用不了props来传递数据。要想把Container接收到的props传给UI组件,就只能用connect的API,通过函数返回对象的形式来传递
首先看这个Container组件,是没有办法去写子组件传值的
也就是<CountTemplate key1={value1} ...>
这种是没有机会写的
所以我们只能依靠connect传值
//引入Count的UI组件
import CountTemplate from "../../components/Count/CountTemplate";
// 引入connect的API
import { connect } from "react-redux";// 使用connect()()创建并暴露一个Count的容器组件
export default connect()(CountTemplate);
props是key-value的形式,所以我们要来模仿这种形式
这里就采用了对象的形式,来模拟这种key-value
{key:value}
或者{key:()=>{函数体}}
也就是说,props的形式都可以模拟
修改一下Container的Count组件
//引入Count的UI组件
import CountTemplatefrom "../../components/Count/CountTemplate";
// 引入connect的API
import { connect } from "react-redux";function a() {// a函数返回的对象中的key就作为传递给UI组件props的key,value就作为传递给UI组件props的value,这个value是状态return { key1: "value1" };
}function b() {// a函数返回的对象中的key就作为传递给UI组件props的key,value就作为传递给UI组件props的value,这个value是函数return {key2: () => {console.log("OK");},};
}
// 使用connect建并暴露一个Count的容器组件,传a,b函数的返回值给UI组件
export default connect(a, b)(CountTemplate);
UI组件获取参数:
在UI组件打印一下:console.log("UI组件接收到的props是", this.props);
所以想获取就更简单了,直接this.props.key
即可
注意,a传值的时候有点小坑,我们应该专注于状态的传递。所以优化一下
function a(state) {// 这里直接传state即可,这个state是从App传过来的// 所有的状态就直接用这个state给传过去了(注意是所有的state)// key1是自定义的,不设限return { key1: state };
}
同样,b里如果要调用dispatch函数,本应该import store
import store from "../../redux/store";
import { createAddAction } from "../../redux/count_action";function b() {// 返回值返回value为函数的对象// add是自定义的,不设限,仅代表当前返回值的函数return {add: (number) => store.dispatch(createAddAction(number)),};
}
但是由于Container里面不应该引入store,并且dispatch可以自动传入,所以就不用通过导入store来调用dispatch了。和上面的state一样,自动传入dispatch
改造后:
//这个是自定义的action函数,不是redux的函数
import { createAddAction } from "../../redux/count_action";function b(dispatch) {// 自动传入了dispatchreturn {// 不再需要用store调用dispatch // 这种就可以简写了add: (number) => store.dispatch(createAddAction(number)),add: (number) => dispatch(createAddAction(number)),};
}
案例总结
demo结构,重点主要集中在Container上
这四个文件照之前的没有任何变化,所以不赘述了
UI组件 CountTemplate.jsx
import React, { Component } from "react";export default class Count extends Component {add = () => {// add操作const { value } = this.selectNumber;// 找到传入函数并传参this.props.add(value * 1);};addNotOdd = () => {// 奇数add操作const { value } = this.selectNumber;if (this.props.key1 % 2 !== 0) {this.props.add(value * 1);}};addAsync = () => {// 异步add操作const { value } = this.selectNumber;setTimeout(() => {this.props.add(value * 1);}, 500);};render() {return (<div><div>当前求和:{this.props.key1}</div><select ref={(c) => (this.selectNumber = c)}><option value="1">1</option><option value="2">2</option><option value="3">3</option></select><div><button onClick={this.add}>add</button><button onClick={this.sub}>sub</button><button onClick={this.addNotOdd}>addNotOdd</button><button onClick={this.addAsync}>addAsync</button></div></div>);}
}
Container组件 Countainer.jsx
import { connect } from "react-redux";
import CountTemplatefrom "../../components/count/CountTemplate";
import { add } from "../../redux/count_action";function a(store) {return { key1: store };
}function b(dispatch) {// 允许传递多个函数return {// add操作的函数add: (data) => dispatch(add(data)),// sub操作的函数sub: (data) => dispatch(sub(data)),};
}// 传入func a(负责state传递) func b(负责函数动作传递)
// 最后桥梁连接CountTemplate组件
export default connect(a, b)(CountTemplate);
App.jsx
import React from "react";
import Container from "./pages/container/Container";
import store from "./redux/store";
import { BrowserRouter } from "react-router-dom";class App extends React.Component {render() {return (<BrowserRouter>{/* 在App向Container传递store(props形式) */}<Container store={store}></Container></BrowserRouter>);}
}
export default App;
index.js
// 引入React核心库
import React from "react";
import { createRoot } from "react-dom/client";
// 引入App标签
import App from "./App";
import store from "./redux/store";// React18版本监听并刷新页面
const root = createRoot(document.getElementById("root"));
root.render(<App />);//监测redux中状态的改变,如redux的状态发生了改变,那么重新渲染App组件
store.subscribe(() => {root.render(<App />);
});
以上就可以完成计算组件
Container命名优化(引出规定的函数名)
传值的函数可以看到,实际上,所有的state和function的参数传递,通过一次就可以完全传递完。
所以react-redux里面就提供好了函数名专门传递state和function,就不需要我们再去单独定义了。更加规范了。
// 函数命名不规范
function a(store) {return { key1: store };
}
// 函数命名不规范
function b(dispatch) {// 允许传递多个函数return {// add操作的函数add: (data) => dispatch(add(data)),// sub操作的函数sub: (data) => dispatch(sub(data)),};
}// 使用connect建并暴露一个Count的容器组件,传a,b函数的返回值给UI组件
export default connect(a, b)(CountUI);
connect 方法是一个连接器,用于连接容器组件和 UI 组件,它第一次执行时,接收4个参数,这些参数都是可选的,它执行的执行的结果还是返回一个函数,第二次执行接收一个 UI 组件
connect方法第一次执行时(connect()
)的四个参数:mapStateToProps
、mapDispatchToProps
、mergeProps
、options
connect方法第二次执行时传入的UI组件connect()(UI组件)
这里先说传state和传方法的两个函数
mapStateToProps函数返回的是一个对象(对象value是state),mapStateToProps用于传递状态
mapDispatchToProps函数返回的是一个对象(对象value是function),mapDispatchToProps用于传递操作状态的方法
这只是官方推荐的API定义方式,后面还有更简写的方式来传递state和function
官方解释:mapStateToProps,mapDispatchToProps
具体在UI获取时候的key,还是需要看return的对象把key定义成什么,才能用this.props来获取key
同时之前的定时加操作是在UI组件里面做的,并没有放在`count_action.js`里面,所以把定时加操作搬进`count_action.js`
优化之后:
Container.jsx
:
import { connect } from "react-redux";
import CountUI from "../../components/count/CountUI";
import { add, sub, addAsync } from "../../redux/count_action";// 传递state
function mapStateToProps(store) {return { key1: store };
}// 传递dispatch
function mapDispatchToProps(dispatch) {// 允许传递多个函数 通过dispatch通知Redux执行函数return {// add操作的函数add: (data) => dispatch(add(data)),// sub操作的函数sub: (data) => dispatch(sub(data)),// addAsync操作的函数,addAsync操作搬进action里addAsync: (data, time) => dispatch(addAsync(data, time)),};
}// 传入func mapStateToProps(负责state传递) func mapDispatchToProps(负责函数动作传递)
// 最后传入CountTemplate组件完成连接
export default connect(mapStateToProps, mapDispatchToProps)(CountUI);
ConuntTemplate.jsx
:
import React, { Component } from "react";export default class Count extends Component {...addAsync = () => {const { value } = this.selectNumber;this.props.addAsync(value * 1, 500);};render() {return (<div>...</div>);}
}
count_action.js
:
/*
该文件专门为Count需要用的Reducer去生成action对象,封装好type,这样在调用组件传一个data进来就可以了
*/
import { ADD, SUB, ADD_ASYNC } from "./constant.js";export function add(data) {// 要返回Action对象(返回type和data数据)return { type: ADD, data };
}export function sub(data) {// 要返回Action对象(返回type和data数据)return { type: SUB, data };
}// 把这个定时操作集成在action里面 => addAsync
// setTimeout(() => {
// this.props.add(value * 1);
// }, 500);
export function addAsync(data, time) {// dispatch为自动传入return (dispatch) => {setTimeout(() => {// 通过dispatch调用add,直接调用是不可以的dispatch(add(data));}, time);};
}
其他的文件没啥变化,不赘述了
写到这里其实 connect
已经比较完善了,但是你可以仔细想想 redux
的工作流程
似乎少了点什么,我们在这里调用了函数,创建了 action
对象,但是好像 store
并没有执行 dispatch
,那是不是断了呢?执行不了呢?
其实这里 react-redux
已经帮我们做了优化,当调用 Action Creator
的时候,会立即发送 action
给 store
而不用手动的 dispatch
。后面马上会用到这个。
总结React-Redux的基本使用
(1).明确两个概念:1).UI组件:不能使用任何redux的api,只负责页面的呈现、交互等。2).容器组件:负责和redux通信,将结果交给UI组件。
(2).如何创建一个容器组件————靠react-redux 的 connect函数connect(mapStateToProps,mapDispatchToProps)(UI组件)-mapStateToProps:映射状态,返回值是一个对象(对象以k:v形式保存state)-mapDispatchToProps:映射操作状态的方法,返回值是一个对象(对象以k:v形式保存多个方法)
(3).备注1:容器组件中的store是靠props传进去的,而不是在容器组件中直接引入
(4).备注2:mapDispatchToProps,也可以是一个对象(只传递一个方法,如果多个方法就需要多个k:v对象)
React-Redux优化
之前的案例,很多功能都是杂糅到一起的,如果体量大起来会让人不知道该怎么办,所以这里在这个案例里面对之前的功能代码进行拆分操作,分文件处理。优化文件结构。
前置知识
例子1:
function mapStateToProps(store) {return { key1: store };}
可以用箭头函数来写
const mapStateToProps = (store)=> {return { key1: store };}
如果箭头函数只有一句并且默认return只有一个对象,就可以省略return,直接箭头指一个括号,就代表return值了 ({ key1: store })
最后优化完
const mapStateToProps = (store)=> ({ key1: store })
例子2:
function mapDispatchToProps(dispatch) {// 多个方法传递return {add: (data) => dispatch(add(data)),sub: (data) => dispatch(sub(data)),addAsync: (data, time) => dispatch(addAsync(data, time)),};
}
按照上面的说法,反正只有一个return,这里就可以直接优化成
mapDispatchToProps=(dispatch)=>{add: (data) => dispatch(add(data)),sub: (data) => dispatch(sub(data)),addAsync: (data, time) => dispatch(addAsync(data, time)),}
带入到connect里面,甚至不用写属性,直接把函数体丢进去即可
简写 mapStateToProps & mapDispatchToProps
对于connect来说,第一次调用connect()
,传入什么名字的函数不重要,传入的位置很重要,connect只认位置上的传入的值。
简写完前后对比:
/*
// 传统写法function mapStateToProps(store) {return { key1: store };}function mapDispatchToProps(dispatch) {// 多个方法传递return {add: (data) => dispatch(add(data)),sub: (data) => dispatch(sub(data)),addAsync: (data, time) => dispatch(addAsync(data, time)),};}
*/// 引入connect生成一个容器组件,连接好React-Redux和UI组件,并暴露
// UI组件中依旧是通过this.props.xxxxxxx读取和操作状态
// export default connect(mapStateToProps, mapDispatchToProps)(CountUI);
export default connect(// 对于第一个参数位置来说,首次调用可以传四个参数[ mapStateToProps 、mapDispatchToProps 、mergeProps、options ]//store没有React-Redux给你自动调用,所以这里要自己写(state) => ({ count: state }), // 等价于mapStateToProps //mapDispatchToProps的简写(key:函数名),甚至不需要标注参数列表//dispatch有React-Redux给你自动调用,所以这里不用写了{ add: add, sub: sub, addAsync: addAsync }//等价于mapDispatchToProps
)(CountUI);
完整开发
目录结构:
首先就是把Contrainer和UI组件放在一起,合并成一个Container文件叫Count。虽然文件是在一起,但是功能和类都是分开的。只是文件放一起了,对外依旧暴露Container,UI这次彻底不用export了。
import { connect } from "react-redux";
import { add, sub, addAsync } from "../../redux/count_action";
import React, { Component } from "react";// 这次CountUI彻底不用暴露了,暴露的任务交给connect来做。connect生成一个对外的Container,同时包含了数据和UI组件
// export default class Count extends Component {
class CountUI extends Component {add = () => {const { value } = this.selectNumber;this.props.add(value * 1);};sub = () => {const { value } = this.selectNumber;if (this.props.key1 === 0 || this.props.key1 < value) {return;}this.props.sub(value * 1);};addNotOdd = () => {const { value } = this.selectNumber;if (this.props.key1 % 2 !== 0) {this.props.add(value * 1);}};addAsync = () => {const { value } = this.selectNumber;this.props.addAsync(value * 1, 500);};render() {console.log(this.props);return (<div><div>当前求和:{this.props.key1}</div><select ref={(c) => (this.selectNumber = c)}><option value="1">1</option><option value="2">2</option><option value="3">3</option></select><div><button onClick={this.add}>add</button><button onClick={this.sub}>sub</button><button onClick={this.addNotOdd}>addNotOdd</button><button onClick={this.addAsync}>addAsync</button></div></div>);}
}/*function mapStateToProps(store) {return { key1: store };}function mapDispatchToProps(dispatch) {// 多个方法传递return {add: (data) => dispatch(add(data)),sub: (data) => dispatch(sub(data)),addAsync: (data, time) => dispatch(addAsync(data, time)),};}
*/// 引入connect生成一个容器组件,连接好React-Redux和UI组件,并暴露
// UI组件中依旧是通过this.props.xxxxxxx读取和操作状态
// export default connect(mapStateToProps, mapDispatchToProps)(CountUI);
export default connect((state) => ({ key1: state }), // 状态{ add: add, sub: sub, addAsync: addAsync } // 方法(不再需要注明参数列表)
)(CountUI);
Provider组件使用
首先看个之前的store传递,很麻烦,需要手动传递,如果每个标签都传递,都需要使用 store 时,很麻烦的
<Count store={store}/>
{/* 示例 */}
<Demo1 store={store}/>
<Demo1 store={store}/>
<Demo1 store={store}/>
<Demo1 store={store}/>
<Demo1 store={store}/>
所以官方就提供了一个大组件Provider,自动可以寻找需要用store的标签,自动精准传递store进去
我们可以这么做:在 src 目录下的 index.js
文件中,引入 Provider
,直接用 Provider
标签包裹 App
组件,将 store
写在 Provider
中即可
import { Provider } from "react-redux";ReactDOM.render(<Provider store={store}><App /></Provider>,document.getElementById("root")
);
这样我们在 App.jsx
文件中,组件无需手写指定 store
,即可使用 store
非常方便
总结React-Redux优化
1.容器组件和UI组件整合一个文件
2.若有多个容器组件,无需自己给每个容器组件传递store,给包裹一个<Provider store={store}>
即可。整体就贡献同一个store了。
3.使用了react-redux后也不用再自己检测redux中状态的改变了,容器组件可以自动完成这个工作。
4.mapDispatchToProps也可以简单的写成一个对象,因为react-redux可以自动dispatch
5.一个组件要和react-redux“打交道”要经过哪几步?
- 1)定义好UI组件—不暴露
- 2)引入connect生成一个容器组件,并暴露,写法如下:
connect(state => ({key:value}), //映射状态{key:xxxxxAction} //映射操作状态的方法
)(UI组件)
- 3)在UI组件中通过
this.props.定义好的key名
读取和操作状态
React-Redux综合案例(多组件组合共享)
引言
在写完了基本的 Redux 案例之后,我们可以尝试一些更实战性的操作,比如我们可以试试多组件间的状态传递,相互之间的交互
如上动图所示,我们想要实现上面的案例,采用纯 React 来实现是比较困难的,我们需要很多层的数据交换才能实现,但是我们如果采用 Redux 来实现会变得非常简单
因为 Redux 打通了组件间的隔阂,完成了复杂组件的数据交互,我们可以自由的进行数据交换,所有存放在 store
中的数据都可以实现共享,那我们接下来看看如何实现的吧~
整合UI组件与容器组件
如果UI组件和容器组件都分开写,那么实际上文件数量就得成倍增长,为了优化就整合一下两个文件,把UI组件和容器组件 放在一起。实际开发中也是整合的。
首先,一个jsx文件里面可以定义多个组件,其实思想上还是容器组件往UI组件传props,只不过两个组件放在了同一个文件里,对外只暴露一个Container接口。
这里演示一下两个或者多个组件在同一个文件里
创建一个Test组件
import React, { Component } from "react";// 默认对外暴露的容器组件
export default class Test extends Component {render() {return <div>Test</div>;}
}// UI组件,不对外暴露,但是可以在自己文件里面用
class Test2 extends Component {render() {return <div>UI组件</div>;}
}
// 他俩之间的连接靠connect
// 这是个简单的connect,后面有详细的描述
export default connect((state) => ({对象}),{// action动作}
)(CountUI);
所以以后就可以只留一个Container文件夹放容器组件
UI组件只要想去拿Redux的状态,就直接找内部的connect,因为这个connect已经拿到了Redux的所有state
后面有详细的描述
重新安排Redux文件结构
如果有Person和Count的组件,每个组件对应的action和reducer这两个文件,都是成倍的增长,所以肯定不能这么一大堆罗列于此,就要把Redux文件夹分层
分层后的Redux,将action文件和reducer文件分开
就不用再叫 组件_action.js
或 组件_reducer.js
的这种了,因为已经在action文件夹下面了,根据文件夹就能分辨出来
React-Redux结构 & 合并Reducer
React-Redux结构
Redux里用的存储结构:
如果是只存了一个值,比如数字这种,那就是不用key去取,直接就this.store即代表着当前对象。
如果是存了两个值以上,n个对象,那就是Redux作为一个大对象,里面存了n多个小对象,通过key来获取目标对象(key是合并reducer时定义的)
合并Reducer
之前的store中只注册了一个Reducer并导出,对于多个Reducer来说,只导出一个肯定是不行的,所以我们要将其他Reducer也注册到store里面,并且命名好key来帮助后续获取
这里提一个前置知识:combineReducers({对象})
传入的对象,就是Redux保存的总对象
重要事情说三遍~
combineReducers({对象})
传入的所有对象,就是Redux保存的总对象
combineReducers({对象})
传入的所有对象,就是Redux保存的总对象
combineReducers({对象})
传入的所有对象,就是Redux保存的总对象
合并后的store.js:
//引入store创建,以及中间件
import { combineReducers,createStore } from "redux";
//引入为store服务的Reducer
import countReducer from "../redux/reducer/count";
import personReducer from "../redux/reducer/person";// 之前的store只能注册一个reducer,也就是count的countReducer
// 没法注册person的reducer,所以我们需要用函数把两个函数都注册了
// const store = createStore(countReducer, applyMiddleware(thunk));// 合并
const allReducer = combineReducers({// sum就是countReducer的key,sum: countReducer,// persons就是personReducer的key,persons: personReducer,
});
//全局只暴露这个Reducers,同样需要用createStore函数创建
export default createStore(allReducer);
Container通过key获取对应的状态
Count组件的connect
// 使用connect建并暴露一个Count的容器组件,传a,b函数的返回值给UI组件
/*(state) => ({ sumNumber: state.sum })对应的含义:(默认传入的全局Redux对象) => ({ 自定义的在UI组件使用的名字: 默认传入的全局Redux对象.目标对象的key })
*/
export default connect(// 这里还获取了Person组件的人数长度(通过Redux获取其他组件的值)// 这里可以传入多个 k-v ,用逗号分隔(state) => ({ sumNumber: state.sum, personLength: state.persons.length }),{// action动作和以前一样传递即可add: add,sub: sub,addAsync: addAsync,}
)(CountUI);
Person组件的connect
/*(state) => ({ personList: state.persons }),对应的含义:(默认传入的全局Redux对象) => ({ 自定义的在UI组件使用的名字: 默认传入的全局Redux对象.目标对象的key }),
*/
export default connect(// 这里还获取了Count组件的求和值(通过Redux获取其他组件的值)// 这里可以传入多个 k-v ,用逗号分隔(state) => ({ personList: state.persons, countSum: state.sum }),{// 这里绑定的action该咋传咋传addPerson: addPerson,}
)(PersonUI);
整个案例代码
先看结构:
容器组件Count.jsx:
// 引入connect的API
import { connect } from "react-redux";
import { add, sub, addAsync } from "../../redux/action/count";import React, { Component } from "react";class CountUI extends Component {add = () => {const { value } = this.count;this.props.add(value);};sub = () => {const { value } = this.count;this.props.sub(value);};addNotOdd = () => {if (this.props.sumNumber % 2 !== 0) {this.props.add(value);}};addAsync = () => {const { value } = this.count;this.props.addAsync(value, 500);};render() {console.log(this.props);return (<div><h1>当前结果{this.props.sumNumber},下方组件总人数为{this.props.personLength}</h1>选择内容<select ref={(c) => (this.count = c)}><option value={1}>1</option><option value={2}>2</option><option value={3}>3</option></select><button onClick={this.add}>+</button><button onClick={this.sub}>-</button><button onClick={this.addNotOdd}>当前奇数加</button><button onClick={this.addAsync}>非同步加</button></div>);}
}// 使用connect建并暴露一个Count的容器组件,传a,b函数的返回值给UI组件
/*(state) => ({ sumNumber: state.sum })对应的含义:(默认传入的全局Redux对象) => ({ 自定义的在UI组件使用的名字: 默认传入的全局Redux对象.目标对象的key })
*/
export default connect(// 这里还获取了Person组件的人数长度(通过Redux获取其他组件的值)// 这里可以传入多个 k-v ,用逗号分隔(state) => ({ sumNumber: state.sum, personLength: state.persons.length }),{// action动作和以前一样传递即可add: add,sub: sub,addAsync: addAsync,}
)(CountUI);
容器组件Person.jsx:
import React, { Component } from "react";
import { addPerson } from "../../redux/action/person";
import { nanoid } from "nanoid";
import { connect } from "react-redux";class PersonUI extends Component {addPerson = () => {const name = this.name.value;const age = this.age.value;if (name === "" || age === "") {return;}const newPeson = { id: nanoid(), name, age };this.props.addPerson(newPeson);};render() {return (<div><h1>我是person组件,上方组件求和为:{this.props.countSum}</h1><inputref={(name) => {this.name = name;}}></input><inputref={(age) => {this.age = age;}}></input><button onClick={this.addPerson}>add New Person</button><ul>{this.props.personList.map((p) => {return (<li key={p.id}>name: {p.name}---- age: {p.age}</li>);})}</ul></div>);}
}/*(state) => ({ personList: state.persons }),对应的含义:(默认传入的全局Redux对象) => ({ 自定义的在UI组件使用的名字: 默认传入的全局Redux对象.目标对象的key }),
*/
export default connect(// 这里还获取了Count组件的求和值(通过Redux获取其他组件的值)// 这里可以传入多个 k-v ,用逗号分隔(state) => ({ personList: state.persons, countSum: state.sum }),{// 这里绑定的action该咋传咋传addPerson: addPerson,}
)(PersonUI);
Action下的count.js:
import { ADD, SUB } from "../constant.js";
import store from "../store.js";export const add = (data) => ({ type: ADD, data });
export const sub = (data) => ({ type: SUB, data });// 异步action,就是只action的值为函数,在异步action中,一般都会调用同步action,异步action不是必须要用的
// 能在这里写函数,是因为在store做了redux-thunk的设置
export const addAsync = (data, time) => {return () => {// 这里其实只套了一个定时操作setTimeout(() => {// 调用已经定义好的增加actionstore.dispatch(add(data));}, time);};
};
Action下的person.js:
import { ADD_PERSON } from "../constant";export const addPerson = (data) => ({ type: ADD_PERSON, data });
reducer下的count.js:
import { ADD, SUB } from "../constant.js";export default function countReducer(previousState, action) {// 从Action对象里传来的action对象,里面包含type和data,所以我们需要解构出来。等待后续处理const { type, data } = action;if (previousState === undefined) {return 0;}// 判断传入功能类型switch (type) {case ADD://这里用原来的值+新传入的值,得到新值return previousState + data * 1;case SUB:if (previousState < data * 1) {return previousState;}//这里用原来的值-新传入的值,得到新值return previousState - data * 1;// 提一嘴,这里都是return,所以不用breakdefault:return previousState;}
}
reducer下的person.js:
import { ADD_PERSON } from "../constant.js";const initPersonList = [{ id: "123456", name: "Tom", age: "20" }];export default function personReducer(previousState = initPersonList, action) {const { type, data } = action;switch (type) {case ADD_PERSON:// 把之前的展开(之前的数组是没展开的),把新来的加进去return [data, ...previousState];default:return previousState;}
}
常量constant.js
export const ADD = "add";
export const SUB = "sub";
export const ADD_PERSON = "addPerson";
store.js
//引入store创建,以及中间件
import { applyMiddleware, combineReducers, createStore } from "redux";
//引入为store服务的Reducer
import countReducer from "../redux/reducer/count";
import personReducer from "../redux/reducer/person";
import thunk from "redux-thunk";// 之前的store只能注册一个reducer,也就是count的countReducer
// 没法注册person的reducer,所以我们需要用函数把两个函数都注册了
// const store = createStore(countReducer, applyMiddleware(thunk));// 合并
const allReducer = combineReducers({// sum就是countReducer的key,sum: countReducer,// persons就是personReducer的key,persons: personReducer,
});// 全局只暴露这个Reducers,同样需要用createStore函数创建
// 同时允许接收函数的applyMiddleware(thunk)也不能丢了
export default createStore(allReducer, applyMiddleware(thunk));
App.jsx
class App extends React.Component {render() {return (<div><Provider store={store}>{/* 用Provider组件的props传递store进入Container组件 */}// 被Provider组件包裹的子组件都能自动接收到store// 这俩都是容器组件<Count></Count><Person></Person></Provider></div>);}
}
export default App;
发现个小bug:
原因是没有弄redux-thunk做中间件支持,再详细可以看这个:redux-thunk
总结React-Redux综合案例
(1).定义一个Pserson组件,和Count组件通过redux共享数据。
(2).为Person组件编写:reducer、action,配置constant常量。
(3).重点:Person的reducer和Count的Reducer要使用combineReducers进行合并,合并后的总状态是一个对象!!!
(4).交给store的是总reducer,最后注意在组件中取出状态的时候,记得“取到位”。
纯函数概念
就是一个概念,没有编码
引言
1.一类特别的函数: 只要是同样的输入(实参),必定得到同样的输出(返回)
2.必须遵守以下一些约束
- 1)不得改写参数数据
- 2)不会产生任何副作用,例如网络请求,输入和输出设备
- 3)纯函数里面不能调用Date.now()或者Math.random()等不纯的方法
3.redux的reducer函数必须是一个纯函数
例子
纯函数要求输入和输出相同
比如:
// a是纯函数
function a(data) {return data;
}
// b不是纯函数
function b(data) {let c = 1;return c;
}
再拿一个reducer举个例子:
这里的 return [data, ...previousState];
。因为地址引用发生了变化,所以就能触发页面更新。因为Redux是浅比较,不比较对象内容,发现return的引用地址变化了就自动更新了。
export default function personReducer(previousState = initPersonList, action) {const { type, data } = action;switch (type) {case ADD_PERSON:// 把之前的展开(之前的数组是没展开的),把新来的加进去return [data, ...previousState];default:return previousState;}
}
如果是非纯函数,比如这样。这就无法生效,因为不是纯函数
export default function personReducer(previousState = initPersonList, action) {const { type, data } = action;
switch (type) {case ADD_PERSON:var newArray = previousState;newArray.push(data);return newArray;default:return previousState;}
}
Redux-DevTools
顾名思义,开发者工具
需要插件+npm库的依赖
npm install redux-devtools-extension --legacy-peer-deps
store需要导入redux-devtools-extension的依赖,并且在暴露store的connect函数的第二个参数位置传入对应的api
如果之前有applyMiddleware(thunk)
的中间件操作,可以选择将中间件传入其api
...省略
// redux-devtools api
import { composeWithDevTools } from "redux-devtools-extension";// 合并
const allReducer = combineReducers({// sum就是countReducer的key,sum: countReducer,// persons就是personReducer的key,persons: personReducer,
});export default createStore(allReducer,// redux-devtools apicomposeWithDevTools(applyMiddleware(thunk))
);
再次启动项目,就可以在浏览器的插件里面看redux存进去的东西了
总结工具使用
(1).yarn add redux-devtools-extension
(2).store中进行配置import {composeWithDevTools} from 'redux-devtools-extension'const store = createStore(allReducer,composeWithDevTools(applyMiddleware(thunk)))
Reducers优化
之前store中,所有的Reducers都是积压在store中进行的合并,如果是大型项目将难以维护,所以我们一般把Reducers的合并,放在一个单独的js文件中去做,完成导出,导出完毕之后在store中引入即可
在reducer下专门新建一个index文件,完成汇总合并操作
/*该文件用于汇总所有的reducer为一个总的reducer
*/
import { combineReducers } from "redux";//引入为store服务的Reducer
import countReducer from "./count";
import personReducer from "./person";
// 合并
const allReducer = combineReducers({// sum就是countReducer的key,sum: countReducer,// persons就是personReducer的key,persons: personReducer,
});// 导出
export default allReducer;
精简之后的store文件,非常清爽
//引入store创建,以及中间件
import { applyMiddleware, createStore } from "redux";
import thunk from "redux-thunk";
import { composeWithDevTools } from "redux-devtools-extension";// 引入reducers
import allReducer from "./reducer";export default createStore(allReducer,composeWithDevTools(applyMiddleware(thunk))
);
总结:
(1).所有变量名字要规范,尽量触发对象的简写形式。
(2).reducers文件夹中,编写index.js专门用于汇总并暴露所有的reducer