写法的变更
之前
const divNode = <div>你好,React</div>
ReactDOM.render(divNode,document.getElementById('root'))
会警告
现在
<script type="text/babel">
const divNode = <div>你好,React</div>
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(divNode)
</script>
同时,不支持渲染对象
const array = ['动感超人','西瓜超人'];//支持
const listObj = [
{name:'李白',sex:'男'},
{name:'李白2',sex:'男'},
]
const divNode = <div>你好,React {listObj}//不支持</div>
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(divNode)
引入
import ReactDOM from "react-dom/client"
Card封装为UI
- 函数标签体
- 传递类名
const Logs = () => {
return (
<Card className="logs">
<LogItem/>
</Card>
)
};
/*Card设置为其添加阴影效果*/
const Card = (props) => {
return (
<div className={`card ${props.className}`}>
{props.children}
</div>
);
};
表单的双向绑定
- 通过setState和onChange来实现,
非受控 没有使用state,受控,使用了state并绑定了值
<Card className="login-form">
<div className='login-form_item'>
<label >
<span className='label' >日期:</span>
<input type='date' value={inputDate} onChange={(e) => setInputDate(e.target.value)}/>
</label>
</div>
<div className='login-form_item'>
<label>
<span className='label'>内容:</span>
<input type='text' placeholder='请输入学习内容' value={inputContent} onChange={e => setInputContent(e.target.value)}/>
</label>
</div>
<div className='login-form_item'>
<label>
<span className='label'>时长:</span>
<input type='text' placeholder='请输入学习时长' value={inputTime} onChange={e => setInputTime(e.target.value)}/>
</label>
</div>
<div className='login-form_operation'>
<button className='login-form_operation_btn'>添加计划</button>
</div>
</Card>
数据为true的时候才会显示对话框
别漏掉了大括号哦
解决对话框层级问题
-
出现情况:组件默认作为了父组件的后代,被渲染到了页面当中导致出现层级问题
-
解决办法
- 和根元素平级
- 使用protal把组件渲染到网页指定位置
-
使用方法
index.html
添加一个容器
<div id='portal-root '></div>
- 在使用的地方获取容器DOM
const protalDOM = document.getElement('portal-root'); const Backdrop = () => { return ReactDOM.createProtal(jsx内容,目标位置) } const divPortal = document.getElementById('portal-root'); return ReactDOM.createPortal(( <div className='selfMask'> {props.children} </div> ),divPortal)
添加过滤功能
- 注意字符串的问题,因为这里用的是全等于号,所以在传出去的时候转化为了数字
const handleChange = (e) => {
/*传递值*/
console.log(e.target.value)
props.onChange(+e.target.value);
}
return (
<div>
<select value={props.defaultValue} onChange={handleChange}>
<option value={2022}>2022</option>
<option value={2021}>2021</option>
<option value={2020}>2020</option>
</select>
</div>
);
create-react-app
React.StrictMode
作用- 使用严格模式去渲染组件
内联样式和行内样式
- 这个可以参考,根据state的值动态设置样式
import "./App.css"
会全局污染吗?会的,引入是全局的,其他组件也可以看到import "./App.css"
就是全局引入,在App.js当中可以看到,其他组件也都可以使用App.css当中的类名
模块化css(避免全局污染 )
- 创建一个xxx.module.css
.p1{
color:red
}
.app_wrapper{
color:red
}
.app_wrapper_info {
background-color: green;
}
.app_wrapper_name {
background-color: blue;
}
- 组件中引入
//一般引入的名称为classes
import classes from "./App.module.css";
- 通过classes来设置类
注意,最好不要出现除字母,数字,下划线以外的类名
比如出现app-wrapper_name就不要出现'-'了
- 如果通过.module.css包含标签,比如直接设置div为棕色,依旧会影响到全局组件
视口的设置
Context
- 避免组件逐层传来传去,就可以使用context
一种组件通信方式,常用于[祖组件]
和 [后代组件]
间的通信
处理字体大小
-
在小手机是正常的,但是切换到大屏幕的手机就字体就有问题了
-
原因
- 像素比问题
- 所以不要将字体设置为px,设置为rem即可
-
但是假如你使用了转换插件,并且在input当中的placeholder的时候没有设置字体大小,就会出现这种情况,所以,最好的预防方法就是将出现字体的地方都设置下字体大小
public/img和src/asset
- public下的可以被服务器所访问,也就是有专门的地址可以看到这个
- src/asset不可以被服务器访问,也就是没有入口可以看到这个
避免事件冒泡
- 在div不想被取消回调的div身上添加如下
<div className={classes.detail} onClick={e => e.stopPropagation()}>
遮罩层
- 遮罩层如果需要接收所有的参数,可以这样子写
<div {...props} className={`${classes.selfMask} ${props.className}`} >
{children}
</div>
- 但是注意,
{...props}
要写在前面哦,如果写在后面,会覆盖已经书写的className
属性
项目很多情况出现冒泡
- 很多情况出现冒泡,都需要在父元素身上绑定下
stopPropagation
e.stopPropagation
<div className={classes.confirm_operation} onClick={e=>e.stopPropagation()}>
<button className={classes.confirm_operation_cancel} onClick={cancel}>取消</button>
<button className={classes.confirm_operation_clear} onClick={confirm}>清空</button>
</div>
清空购物车后数量没有变化
-
因为之前的浅拷贝,用的都是从最原始的那个数据,所以想要解决清空购物车后数量 依旧存在的问题,以下二个方法
- 使用深拷贝(但是这样子原始数据数量就不会改变了在这个项目里面)
- 清空购物车的时候,手动清空下里面的amount
//手动清空amount数量 temp.list.forEach(item => item.amount = 0); temp.total = 0; temp.list = []; temp.money = 0; setCartData(temp)
useEffect和React严格模式和setState执行流程
- React组件有部分逻辑都可以直接编写到组件的函数体中的,像是对数组调用filter、map等方法,像是判断某个组件是否显示等。但是有一部分逻辑如果直接写在函数体中,会影响到组件的渲染,这部分会产生“副作用”的代码,是一定不能直接写在函数体中。
- 例如,如果直接将修改state的逻辑编写到了组件之中,就会导致组件不断的循环渲染,直至调用次数过多内存溢出。
React.StrictMode
- 编写React组件时,我们要极力的避免组件中出现那些会产生“副作用”的代码。同时,如果你的React使用了严格模式,也就是在React中使用了
React.StrictMode
标签,那么React会非常“智能”的去检查你的组件中是否写有副作用的代码,当然这个智能是加了引号的,我们来看看React官网的文档是如何说明的:
Strict mode can’t automatically detect side effects for you, but it can help you spot them by making them a little more deterministic. This is done by intentionally double-invoking the following functions:
- Class component
constructor
,render
, andshouldComponentUpdate
methods - Class component static
getDerivedStateFromProps
method - Function component bodies
- State updater functions (the first argument to
setState
) - Functions passed to
useState
,useMemo
, oruseReducer
上文的关键字叫做“double-invoking”即重复调用,这句话是什么意思呢?大概意思就是,React并不能自动替你发现副作用,但是它会想办法让它显现出来,从而让你发现它。那么它是怎么让你发现副作用的呢?React的严格模式,在处于开发模式下,会主动的重复调用一些函数,以使副作用显现。所以在处于开发模式且开启了React严格模式时,这些函数会被调用两次:
类组件的的 constructor
, render
, 和 shouldComponentUpdate
方法
类组件的静态方法 getDerivedStateFromProps
函数组件的函数体
参数为函数的setState
参数为函数的useState
, useMemo
, or useReducer
重复的调用会使副作用更容易凸显出来,你可以尝试着在函数组件的函数体中调用一个console.log
你会发现它会执行两次,如果你的浏览器中安装了React Developer Tools,第二次调用会显示为灰色。
-
比如报这个错误
Too many re-renders
就是我们直接在函数体中调用setState
方法,就会触发- 疑问:不是说,当新的state值和旧值相同的时候,不会触发组件的重新渲染,为什么下面这个代码会呢?
- 解释:
setCount
被调用的时候,处于渲染阶段,不会判断值是否相同
const App = () =>{ const [count,setCount] = useState(0); setCount(0) return ( <div> {count} </div ) }
函数组件setState(或者是setXXXXX)执行流程
setState() --> dispatchSetData()
//1.会先判断,组件当前处于什么阶段
如果是渲染阶段 --> 不会检查state的值是否相同
如果不是渲染阶段 --> 会检查state的值是否相同
- 如果值不相同,则会对组件进行重写渲染
- 如果值相同,则不会组件进行重写渲染
如果值相同,React在一些情况下会继续执行当前组件的渲染
但是这个渲染不会触发其子组件的渲染,这次渲染不会产生实际的效果
这种情况通常发生在值第一次相同的时候
示例(非严格模式下)
App.jsx
import React, {useState} from 'react';
import B from "./B";
const App = () => {
console.log('App组件重新渲染了')
const [count,setCount] = useState(0);
// handleClick被点击触发的时候
// 是在非渲染阶段了
const handleClick = () => {
setCount(1);
/**
* 初始化的时候
* 输出 'App组件重新渲染了' 和 'B组件被重新渲染了'
*
* 第一次点击按钮 count 变为 1
* 输出 'App组件重新渲染了' 和 'B组件被重新渲染了'
*
* 第二次点击按钮 count 变为 1
* 输出 'App组件重新渲染了'
*
* 第三次点击按钮 count 变为 1
* 未输出任何内容
*/
}
return (
<div>
我是App{count}
<button onClick={handleClick}>点击我</button>
<B/>
</div>
);
};
export default App;
B.jsx
import React from 'react';
const B = () => {
console.log('B组件被重新渲染了')
return (
<div>
我是B组件
</div>
);
};
export default B;
还有这个示例也可以看看,setTimeout是异步的,执行的时候也是处于非渲染阶段,所以这个时候就不会导致重复调用从而产生Too many re-renders的报错
import React, {useState} from 'react';
const App = () => {
console.log('App组件重新渲染了')
const [count,setCount] = useState(0);
setTimeout(() => {
setCount(1)
},0)
return (
<div>
我是App{count}
</div>
);
};
export default App;
useEffect
-
useEffect
是一个钩子函数,需要一个函数作为参数,这个作为参数的函数,默认将会在组件渲染完毕后执行(也就是在非渲染阶段才执行这个回调) -
在实际开发中,可以将那些会产生副作用的代码编写到
useEffect
的回调函数中,这样子就可以避免这些代码影响到组件的渲染 -
说通俗点就是我不可以在渲染阶段去修改state的值,我需要在他渲染完毕后再去修改(就是变根回调的执行时机)
-
如果**不想每次都在渲染阶段useEffect被调用,**我们可以传入第二个参数
第二个参数为数组(即为依赖项目)
当数组当中的依赖项发送改变的时候,useEffect才会去调用
通过会将Effect当中所有的变量都设置为依赖项目
这样子一来可以确保这些值发生变化时候,会触发Effect变化
像setState()是由钩子函数useState()生成的
useState()会确保组件的每次渲染都会获取到相同的setState()对象,所以stateState可以不设置进Effect
所以下面的setShowDetail,setShowCheckOut,可以写进去,也可以不写进去
当第二个参数为空数组,则意味着Effect只会在组件初始化的时候执行一次
const a = 10;
//产生一个闭包
useEffect(() => {
//确保a为最新值
console.log(a);
setShowDetail(false);
setShowCheckOut(false);
},[a,setShowDetail,setShowCheckOut])
- 结合天俞老师的笔记
React中的副作用操作
* 发ajax请求获取数据
* 设置订阅 / 启动定时器
* 手动更改真实DOM
import React from "react";
React.useEffect(() => {
//do something
// ...
//返回的函数将在组件卸载前执行
return () => {
//在这里做一些收尾工作,会在下一次effect调用前执行
//比如清除定时器,取消订阅
}
},[stateValue]) //如果指定的是 [], 那么回调函数只会在第一次render后执行
//否者就会里面的值发生变化,就执行一次回调(因为render被重新执行了)
- 总结
- 如果想每次渲染都执行,第二个参数什么都不写
- 如果想只当使用到的变量发生改变重新调用,那么第二个参数以数组的形式传入使用到的变量名(也就是写入依赖项目)
- 如果想只执行一次(初始化的时候执行),第二个参数传入空数组
[]
通过useEffct改造input
- 注释掉的部分为改造前
import React, {useEffect, useState} from 'react';
import classes from "./index.module.css";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faSearch} from "@fortawesome/free-solid-svg-icons";
//import {throttle} from "lodash";
const Index = (props) => {
/*使用节流*/
//const handleChange = throttle((e) => {
// props.search(e.target.value);
//},150)
const [value,setValue] = useState('')
const handleChange = (e) => {
setValue(e.target.value.trim());//设置值
}
useEffect(() => {
props.search(value);
},[value])
return (
<div className={classes.filter}>
<div className={classes.filter_search}>
<FontAwesomeIcon className={classes.filter_search_icon} icon={faSearch} />
{/*<input type="text" className={classes.filter_search_input} placeholder={'请输入关键字'} onChange={handleChange}/>*/}
<input type="text" value={value} onChange={handleChange} className={classes.filter_search_input} placeholder={'请输入关键字'} />
</div>
</div>
);
};
export default Index;
疑问
- 之前的时候,我设置购物车数量+1,总是会出现多加一次的情况
/*增加*/
setCartData(prevState => {
const temp = {...prevState};
console.log(temp.list.includes(item))
if(!temp.list.includes(item)){
console.log('我是不包含')
//不包含
item.amount = 1;
temp.list.push(item);
}else{
console.log('我是包含')
//包含
console.log('之前',item.amount)
item.amount +=1;
console.log('之后',item.amount)
}
temp.total++;//数量+1
return temp;
})
我点击+号之后,再次点击+号,总是会重复调用
因为开启了严格模式,会去检查是否有副作用,就会调用二次(因为有时候调用二次副作用就凸显了出来)
useReducer
- 操作useState生成的数据,在多个函数对state设置了(state定义和方法的添加不在同一个地方),如下图就是这种情况
//伪代码-1示例
const [carData,setCatData] = useState({})
//添加商品
const addItme = () => {};
//减少商品
const removeItme = () => {};
//清空购物车
const clearCart = () => {};
//伪代码-2示例
import React, {useState} from 'react';
const App = () => {
const [number,setNumber] = useState(0);
const handleReduce = () => {
setNumber(prevState => prevState - 1);
}
const handleAdd = () => {
setNumber(prevState => prevState + 1);
}
return (
<div>
<button onClick={handleReduce}>-</button>
<span>{number}</span>
<button onClick={handleAdd}>+</button>
</div>
);
};
export default App;
- 所以我们可以使用
useReducer
useReducer(reducer,initialArg,init)
参数:
reducer:为一个函数(state,action) => stateValue
reducer在执行的时候,会收到二个参数
* state 当前最新的state
* action(一般叫action) 普通的js对象,可以将要干的事情告诉action,
* 进而根据action中不同值来执行不同操作
* 为了避免type无效(区分操作的type),可以无论如何都会返回一个值
* 说白了reducer就是将对state的不同操作定义在同一个函数当中
initialArg: state的初始值,作用和useState()中的值是一样的
返回值:为数组
第一个参数: state 用来获取state的值
第二个参数:state修改的派发器
* 通过派发器可以发送操作state的命令
* 具体的修改行为将会由另外一个函数(reducer)执行
- 说通俗点就是dispatch是一个派活的,reducer是干活的,reducer根据指令不同,从事不同工作
//可以通过...arg 后输出arg查看所有参数
const [count,countDispatch] = = useReducer((state,action) => {
const { type } = action.type;
if(type === 'ADD') {
return state + 1;
}else if(type ==='SUB'){
return state - 1;
}
return state;//避免没有type匹配失败导致问题
},1)
//或者
const [count,countDispatch] = = useReducer((state,action) => {
const { type } = action.type;
switch(type){
case 'ADD': return state + 1;
case 'SUB': return state -1;
default: return state;
}
},1)
import React, {useState,useReducer} from 'react';
const reducer = (prevState,action) => {
const {type} = action;//对传递过来的对象进行结构
switch (type){
case 'SUB': return prevState - 1;
case 'ADD': return prevState + 1;
default: return prevState;//避免未知的
}
}
const App = () => {
/* const [number,setNumber] = useState(0);
const handleReduce = () => {
setNumber(prevState => prevState - 1);
}
const handleAdd = () => {
setNumber(prevState => prevState + 1);
}*/
const [number,numberDispatch] = useReducer(reducer,0)
return (
<div>
{/*点击按钮,调用指挥者(dispatch),指挥者再去通知reducer执行对应操作*/}
<button onClick={() => numberDispatch({type:'SUB'})}>-</button>
<span>{number}</span>
<button onClick={() => numberDispatch({type:'ADD'})}>+</button>
</div>
);
};
export default App;
- reduce通常会定义在组件的外部,避免重复创建(注意,是Reducer创建在外面,不是useReducer创建在外面,否则useReudcer创建在外面会提示React Hook “useReducer” cannot be called at the top level. React Hooks must be called in a React function component or a custom React)
//之前
const App = () => {
const [count,countDispatch] = useReducer(() => {
},0)
}
//修改之后
const reducer = () => { ... }
const App = () => {
const [count,countDispatch] = useReducer(reducer,0)
}
React.memo()
- 组件被重新渲染有二种情况
- 第一种: 组件的state状态改变导致重新渲染
- 第二种: 父组件重新渲染导致子组件重新渲染
针对下面这种情况,我们就可以使用React.memo,可以看到,在没有使用React.memo
的时候,App的值被改变,子组件也跟着改变了
-
有时候这种渲染很不好,比如我子组件什么都没有变化,就是因为父组件变化了,我就要变化,所以为了避免重复渲染导致性能上的问题,我们可以使用
React.memo
-
React.memo()是一个高阶函数
- 高阶函数就是参数是函数或者返回值是函数的函数
-
经过React.memo()包装过的新组件具有缓存功能
- 包装过后,只有当组件的
props
发生变化后才会触发组件的重新渲染,否则总是返回缓存中结果
- 包装过后,只有当组件的
-
示例(非严格模式下)
App.jsx
import React, {useState} from 'react';
import A from "./component/A";
const App = () => {
console.log('App被渲染了')
const [number,setNumer] = useState(0);
const handleClick = () => {
setNumer(prevState => prevState + 1);
}
return (
<div>
我是App
<button onClick={handleClick}>点击我+1</button>
{number}
<A/>
</div>
);
};
export default App;
A.jsx
import React from 'react';
import B from "./B";
const A = () => {
console.log('A被渲染了')
return (
<div>
我是A组件
<B/>
</div>
);
};
export default React.memo(A);
B.jsx
import React from 'react';
const B = () => {
console.log('B被渲染了')
return (
<div>
我是B组件
</div>
);
};
export default React.memo(B);
效果:点击+1多少次,子组件都不会被重新渲染
useCallback
- 使用React.memo后,的确可以做到缓存的功能,但是如果子组件接收了来自父组件通过props传递的函数,在使用React.memo后会发生什么?(复习下React.memo,发生props变化的时候才会重新渲染,否则就不会重新渲染)
App.jsx
import React, {useState} from 'react';
import A from "./component/A";
const App = () => {
console.log('App被渲染了')
const [number,setNumer] = useState(0);
const handleClick = () => {
setNumer(prevState => prevState + 1);
}
return (
<div>
我是App
<button onClick={handleClick}>点击我+1</button>
{number}
<A add={handleClick}/>
</div>
);
};
export default App;
A.jsx
import React from 'react';
const A = (props) => {
console.log('A被渲染了')
return (
<div>
我是A组件
<button onClick={props.add}>点击我操作App,使其数字+1</button>
</div>
);
};
export default React.memo(A);
效果图,可以看到,即使使用了React.memo
,A组件接收了父组件传递过来的值的时候,依旧会因为父组件的改变而导致组件重新渲染(因为props改变了嘛)
- 上述代码示例情况就是因为A组件的重新渲染导致
handleClick
被重新创建,导致发生改变,所以我们可以使用useCalback
来创建react中的回调函数去避免这种情况 useCallback
是一个钩子函数,用来创建react中的回调函数useCallback
创建的回调函数不会在组件重新渲染时重新创建useCallback
参数
参数:
参数1:回调函数
参数2:依赖数组
当依赖数组的变量发生变化时,回调函数才会重新执行
如果不指定依赖数组(也就是第二个参数什么都不写),
回调函数每次被渲染的时候就会被重新执行
一定要将回调函数中使用到的所有变量设置到依赖当中
除了setState,因为这个不会变~
-
总结
- 如果想每次渲染都执行,第二个参数什么都不写
- 如果想只当使用到的变量发生改变重新调用,那么第二个参数以数组的形式传入使用到的变量名(也就是写入依赖项目)
- 如果想只执行一次(初始化的时候执行),第二个参数传入空数组
[]
-
示例
import React, {useCallback, useState} from 'react';
import A from "./component/A";
const App = () => {
console.log('App被渲染了')
const [number,setNumer] = useState(0);
const handleClick = useCallback(() => {
setNumer(prevState => prevState + 1);
},[])
//const handleClick = () => {
// setNumer(prevState => prevState + 1);
//}
return (
<div>
我是App
<button onClick={handleClick}>点击我+1</button>
{number}
<A add={handleClick}/>
</div>
);
};
export default App;
可以看到,即使App发生了重写渲染,A组件也不会重新渲染,因为App当中的callBack我们设置为了只会在创建的时候执行一次
(todo)Strapi的使用
使用fetch
- 这里简单记录下,具体的可以看@mdn-fetch
- 需要注意的是
- 也就除了网络故障时和请求被阻止,其他情况都是resolve
- 所以我们可以通过判断
ok
来进行相应的操作
当接收到一个代表错误的 HTTP 状态码时,从 fetch() 返回的 Promise 不会被标记为 reject,即使响应的 HTTP 状态码是 404 或 500。相反,它会将 Promise 状态标记为 resolve(如果响应的 HTTP 状态码不在 200 - 299 的范围内,则设置 resolve 返回值的 ok 属性为 false),仅当网络故障时或请求被阻止时,才会标记为 reject。
- 我们输出查看下请求返回的结果
fetch('http://localhost:3000/list')
.then(res => {
console.log(res)
})
.catch(error=>{
})
- 所以可以这样子做,处理数据和错误
fetch('http://localhost:3000/list1414')
.then(res => {
const {ok} = res;//获取请求结果
if(ok){
//请求成功
return res.json();
}
throw new Error('网络异常')
})
.then(res => {
console.log(res.data
);//根据接口不同而返回的数据不同
})
.catch(error=>{
//error.message获取错误文本信息
console.log(error.message);//输出错误信息
})
- 示例student没有做任何逻辑修改,所以只列出app.jsx内容
import React, {useState,useEffect} from 'react';
import StudentList from "./components/StudentList";
import './App.css';
const App = () => {
const [stuData, setStuData] = useState([]);
const [isLoading,setLoading] = useState(false);//是否正在加载
const [isError,setError] = useState(false);//是否有错误
useEffect(() => {
setLoading(true);
setError(false)
fetch('http://localhost:3000/list')
.then(res => {
const {ok} = res;//获取请求结果
setLoading(false);
if(ok){
//请求成功
return res.json();
}
throw new Error('网络异常')
})
.then(res => {
setStuData(res);
})
.catch(error=>{
setError(true)
console.log(error.message);//输出错误信息
})
},[])
return (
<div className="app">
{/*不处于加载的时候就展示列表*/}
{ !isLoading && !isError && <StudentList stus={stuData}/> }
{/*加载状态*/}
{ isLoading && '加载数据中...' }
{/*是否出错*/}
{isError && '数据出错...'}
</div>
);
};
export default App;
- 当然,你也可以使用
async 和 await
- 注意;useEffect的第一个参数不能是异步的
- 我现在才知道原来
try catch
后面还有finally
…
import React, {useState,useEffect} from 'react';
import StudentList from "./components/StudentList";
import './App.css';
const App = () => {
const [stuData, setStuData] = useState([]);
const [isLoading,setLoading] = useState(false);//是否正在加载
const [isError,setError] = useState(false);//是否有错误
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
setError(false)
const res = await fetch('http://localhost:3000/list');
if(res.ok){
//请求成功
const data = await res.json();
setStuData(data)
}else{
throw new Error('网络异常')
}
}catch (e){
setError(e)
}finally {
setLoading(false);//结束加载loading
}
}
fetchData();
},[])
return (
<div className="app">
{/*不处于加载的时候就展示列表*/}
{ !isLoading && !isError && <StudentList stus={stuData}/> }
{/*/!*加载状态*!/*/}
{ isLoading && '加载数据中...' }
{/*/!*是否出错*!/*/}
{isError && '数据出错...'}
</div>
);
};
export default App;
自定义钩子(selfHooks)
- 其实自定义钩子就是一个普通函数,只是他的名字需要使用use开头,(自定义钩子就是对其他钩子的封装 )
redux,RTK,RTKQ
reduex核心思想
多个state放置在一起去统一管理
Reducer
对于state的所有操作,都保存到同一个函数
Store
仓库对象,无论是订阅还是派发任务,都是通过store
根据reducer创建对应的store
dispatch(Store里面的方法)
dispatch依旧是告诉reducer怎么去处理state数据
怎么理解从Store里面调用dispatch呢?因为store根据reducer创建的,所以想要操作数据,自然也应该是从store触发
getState(),从store获取数据
因为redux是适合所有的js的,所以是发布者,订阅者模式
所以store.subscribe(回调函数),发生变化的时候就会执行回调函数
function reducer(prestate,action){
preState 即将更新前state的值,reducer的返回值将作为state的新值
action 是一个普通的js对象,可以保存操作的信息
type表示操作类型
其他需要传递的参数,也可以在action设置
}
通常存储的是一个对象
const store = Redux.createStore(reducer,初始值);
//也可以在reducer指定初始值
function reducer(preState = 1 ,action) {.....}
redux在html当中的使用
- 通过点击增加删除来对数字进行操作
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Redux</title>
<script src="https://cdn.bootcdn.net/ajax/libs/redux/4.2.0/redux.min.js"></script>
</head>
<body>
<button onclick="handleClickReduce()">减少</button>
<span id="number">1</span>
<button onclick="handeClickAdd()">增加</button>
<script>
const spanNumber = document.getElementById('number')
// 1.创建reducer
const reducer = (preState,action) => {
const {type} = action;
switch(type){
case 'ADD': return preState + 1 ;
case 'SUB': return preState - 1;
default: return preState;
}
}
// 2.根据reducer创建store
const store = Redux.createStore(reducer,1);
// 4.发生改变的回调
store.subscribe(() => {
console.log('发生改变了',store.getState())
spanNumber.textContent = store.getState();
})
const handleClickReduce = () => {
// 3.store进行改变
store.dispatch({type:'SUB'})
}
const handeClickAdd = () => {
// 3.store进行改变
store.dispatch({type:'ADD'})
}
</script>
</body>
</html>
- (todo)如果需要指定加任意数,通过dispatch传递即可(reducer第二个action其实就是一个普通的对象)
redux的一些缺点
- 如果state过于复杂,将会非常难于维护
- 可以通过对state分组进行解决,通过创建多个reducer,然后将其合并为一个
- state每一次操作,都需要对state进行复制,然后再去修改,如果需要修改第三层的值,那岂不是很复杂
- case后边的常量维护起来比较麻烦
- 后面二个redux也想到了,所以可以使用
Redux Toolkit(RTK)
,RTK可以完全替换掉redux,RTK包含了redux
RTK
- 安装全套(别忘记安装
react-redux
)
# NPM
npm install @reduxjs/toolkit react-redux
# Yarn
yarn add @reduxjs/toolkit react-redux
- 创建对应的仓库片段
import {createSlice} from "@reduxjs/toolkit";
//1. 创建reducer的切片
//需要一个配置对象作为参数,通过对象的不同属性来指定它的配置
//会自动生成action
const stuSlice = createSlice({
name:'stu',//参与自动生成的action
//state的初始值
initialState: {
name:'动感超人',
},
//操作对应的state方法,通过不同方法操作state值
reducers:{
/*这个state是一个state代理,操作这个会影响到store对应的数据*/
setName(state,action){
state.name = '西瓜超人';//直接影响到state数据
}
}
});
//返回 {type: 'stu/setName', payload: '进去的参数'}
console.log(stuSlice.actions.setName('进去的参数'));
输出结果查看stuSlice
- 创建reducer并暴露
import {createSlice,configureStore} from "@reduxjs/toolkit";
//1. 创建reducer的切片
//需要一个配置对象作为参数,通过对象的不同属性来指定它的配置
//会自动生成action
const stuSlice = createSlice({
name:'stu',//参与自动生成的action
//state的初始值
initialState: {
name:'动感超人',
age:18,
},
//操作对应的state方法,通过不同方法操作state值
reducers:{
/*这个state是一个state代理,操作这个会影响到store对应的数据*/
setName(state,action){
state.name = '西瓜超人';//直接影响到state数据
},
setAge(state,action){
}
}
});
//返回 {type: 'stu/setName', payload: '进去的参数'}
//console.log(stuSlice.actions.setName('进去的参数'));
//暴露所有生成action的函数
export const {setName,setAge} = stuSlice.actions;
//创建仓库并暴露
export default configureStore({
//reducer:stuSlice,
//或者传入一个对象
//key值用于标识不同的reducer
//value则传入对应的reducer,
reducer:{
student:stuSlice.reducer,
}
})
- 使用redux
- 传递着
使用Provider
传递store - 接受者
- 通过
useSelectore
获取对应的state - 通过
useDispatch
获取派发操作store的对象(也就是dispatch) 通过暴露的函数生成操作类型和操作值的对象(也就是setName的返回值)
- 通过
- 传递着
src/index.js
import ReactDOM from "react-dom/client"
import App from "./App"
import {Provider} from "react-redux";
import store from "./store/index";
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<Provider store={store}>
<App/>
</Provider>
)
App.jsx
import React from 'react';
import {useSelector,useDispatch} from "react-redux";
import {setName} from "./store";
const App = () => {
const store = useSelector(state => state.student);//获取student的store
const dispatch = useDispatch();//获取派发器对象
const handleClick = () => {
//传递要设置的新名字
dispatch(setName('我是动感超人'+Date.now()))
}
return (
<div>
{store.name}
<button onClick={handleClick}>更改我的名字</button>
</div>
);
};
export default App;
存在的一些缺点和解决办法
- 如果创建多个切片,并且每一个切片reducers都有setName方法,那么会导致暴露出去的方法重名了
- 解决办法:不同的切片不同的文件(其实就是reducers分开)
store/index.js
import {configureStore} from "@reduxjs/toolkit";
import stuReducer from "./stuSlice"
import addressReducer from "./addressSlice"
export default configureStore({
reducer:{
student: stuReducer,
address: addressReducer,
}
})
store/stuSlice.js
import {createSlice} from "@reduxjs/toolkit";
const stuSlice = createSlice({
name:'stu',
initialState:{
name:'傻瓜超人',
age:18,
},
reducers:{
setName(state,action){
state.name = action.payload;
},
setAge(state,action){
state.age = action.payload;
}
}
});
export const {setName,setAge} = stuSlice.actions;
export default stuSlice.reducer;
store/addressSlice.js
import {createSlice} from "@reduxjs/toolkit"
const addressSlice = createSlice({
name:'address',
initialState:{
address:'高老庄'
},
reducers:{
setAddress(state,action){
state.address = action.payload
}
}
})
export const {setAddress} = addressSlice.actions;
export default addressSlice.reducer;
使用
import React from 'react';
import {useSelector,useDispatch} from "react-redux";
import {setName,setAge} from "./store/stuSlice";
import {setAddress} from "./store/addressSlice";
const App = () => {
const { student,address } = useSelector(state => state);
const dispatch = useDispatch();
return (
<div>
<span>姓名:{student.name}</span>
<span>年龄:{student.age}</span>
<span>地址:{address.address}</span>
<button onClick={() => dispatch(setName('张三'))}>设置姓名为"张三"</button>
<button onClick={() => dispatch(setAge(888))}>设置年龄为888</button>
<button onClick={() => dispatch(setAddress('地球村'))}>设置地址为"地球村"</button>
</div>
);
};
export default App;
RTKQ(redux toolkit query)
-
RTK不仅帮助我们解决了state的问题,同时,它还为我们提供了RTK Query用来帮助我们处理数据加载的问题。RTK Query是一个强大的数据获取和缓存工具。在它的帮助下,Web应用中的加载变得十分简单,它使我们不再需要自己编写获取数据和缓存数据的逻辑。
-
Web应用中加载数据时需要处理的问题:
- 根据不同的加载状态显示不同UI组件
- 减少对相同数据重复发送请求
- 使用乐观更新,提升用户体验
- 在用户与UI交互时,管理缓存的生命周期
-
在之前我们就是通过自己每一次发送请求,就创建对应的状态,比如
isLoading
,isError
来处理加载时候的状态的,有了RTKQ,我们就不需要做这些事情了,我们通过RTKQ来帮我们完成这事情 -
更多说明和教学可以看看@李立超的RTKQ
第一步:配置RTKQ
store/studentApi.js
import {createApi, fetchBaseQuery} from "@reduxjs/toolkit/dist/query/react";
//创建Api对象
//createApi()用来创建RTKQ中的API对象
const studentApi = createApi({
//Api的reducer标识,不能和其他Api或reducer重复
reducerPath:'studentApi',
//指定查询的基础信息,发送请求使用的工具
//用来设置发送请求的工具,就是你是用什么发请求,RTKQ为我们提供了fetchBaseQuery作为查询工具,它对fetch进行了简单的封装,很方便,如果你不喜欢可以改用其他工具,这里暂时不做讨论。
baseQuery:fetchBaseQuery({
baseUrl:'http://localhost:3000/'
}),
endpoints:(build) => {
//build是请求的构建器,通过build来设置请求相关的信息
return {
//使用构建器来创建请求对象
getStudents:build.query({
query(arg) {
//用来指定请求的子路径
//可以直接返回请求的子路径,默认为get(axios也是默认get方式)
//也可以返回配置对象
return 'students';
},
//可以在这里将请求过来的数据进行转换并将结果返回
transformResponse(baseQueryReturnValue){
return 转换后的结果
}
})
}
}
})
//api对象创建后,对象中会根据各种方法自动的生成对应的钩子函数
//通过这些钩子函数,可以向服务器发送请求
//钩子函数的命名规则 endpoints中有一个getStudents 结合查询方式=>生成 useGetStudentsQuery
export const { useGetStudentsQuery } = studentApi;
//我们还需要使用store
export default studentApi;
第二步:创建store对象
store/index.js
import {configureStore} from "@reduxjs/toolkit";
import studentApi from "./studentApi";
export default configureStore({
reducer:{
[studentApi.reducerPath]:studentApi.reducer,
},
middleware:getDefaultMiddleware => {
//getDefaultMiddleware调用后会返回所有的默认中间件
//传入studentApi的缓存生效
return getDefaultMiddleware().concat(studentApi.middleware)
}
})
随后主入口文件src/index.js
import ReactDOM from "react-dom/client"
import App from "./App"
import store from "./store"
import {Provider} from "react-redux";
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<Provider store={store}>
<App/>
</Provider>
)
第三步:使用RTKQ
- 输出查看调用api中钩子查询的返回值
- 可以看到,输出了三次,可以简单理解为发送前,发送中,发送完成
- 每一个阶段都会有具体的字段进行标识,比如
isLoading,isFetching,isSuccess,isError
等 - 这样子就可以便于我们去操作(至少我们不用自己写什么isLoading,isError了,RTKQ帮我们完成了)
import React from 'react';
import {useGetStudentsQuery} from "./store/studentApi";
const App = () => {
//调用api查询数据
//这个钩子函数会返回一个对象作为返回值,请求过程中相关数据都在该对象中
const obj = useGetStudentsQuery();//调用Api中的钩子查询数据
console.log(obj)
return (
<div>
我是App
</div>
);
};
export default App;
- 具体使用示例
import React from 'react';
import {useGetStudentsQuery} from "./store/studentApi";
const App = () => {
//调用api查询数据
//这个钩子函数会返回一个对象作为返回值,请求过程中相关数据都在该对象中
const { isLoading,data,isSuccess} = useGetStudentsQuery();//调用Api中的钩子查询数据
return (
<div>
{ isLoading && '加载中...'}
{isSuccess && data.map(item =>
<p key={item.id}>
姓名:{item.name} <br/>
性别:{item.sex} <br/>
年龄:{item.age} <br/>
地址:{item.address} <br/>
</p>)
}
</div>
);
};
export default App;
效果
数据库的结构
编辑信息使用RTK保持最新数据
- 在之前编辑学生数据的时候,是通过props来传递数据的,但是如果有一个人修改了数据我们没有更新,就会导致我们编辑的时候使用的是旧数据,用户体验不好
- 所以我们可以在编辑的时候重新请求服务器,重新查询数据
- 使用RTKQ关键就是如果传参和如何定义接口
//接口定义
const studentApi = createApi({
// ....
endpoints:(build) => {
//build是请求的构建器,通过build来设置请求相关的信息
return {
getStudents:build.query({
query() {
//用来指定请求的子路径
return 'students';
}
}),
getStudentInfo:build.query({
query(id) {
return `students/${id}`;
}
})
}
}
// ....
})
export const { useGetStudentsQuery,useGetStudentInfoQuery} = studentApi;
//传参-使用的时候传参
//传入id,用作查询的参数
const {data,isSuccess} = useGetStudentInfoQuery(props.id);//异步请求
const [formData,setFormData] = useState({
name: props.name ? props.name : "",
sex: props.sex ? props.sex : "男",
age: props.age ? props.age : "",
address: props.address ? props.address : "",
});
//使用useEffect重新渲染数据当状态改变
//为了避免在渲染中操作state导致无限重复渲染,我们选择在useEffect
useEffect(() => {
if(isSuccess){
setFormData({
...data,
})
}
},[isSuccess])
设置缓存时间和数据转换-query
- RTKQ是自带缓存的,默认为60秒,
- 当我们点击修改的时候,会向后台发送请求,此时数据已经是缓存了,我们再次点击修改,数据已经被缓存,所以不会发送请求了,但是如果有人修改了数据,受到缓存的限制,不会重新请求,所以需要我们可以设置缓存时间keepUnusedDataFor,默认是60秒
- 每一个接口查询都可以设置缓存时间,默认为60(单位秒)
const studentApi = createApi({
// ....
endpoints:(build) => {
//build是请求的构建器,通过build来设置请求相关的信息
return {
getStudents:build.query({
query() {
//用来指定请求的子路径
return 'students';
}
}),
getStudentInfo:build.query({
query(id) {
return `students/${id}`;
},
//设置缓存时间为5秒
keepUnusedDataFor:5,
//加工处理请求过来的数据
transformResponse(baseQueryReturnValue){
return 转换后的结果
}
})
}
}
// ....
})
useQuery的返回值查看
先看图,输出useQuery的返回值
-
refetch
函数- 一个函数,用来重新加载数据
-
status
:string - 请求的状态- pedding:数据正在加载
- fulfilled:数据加载完成
-
isFetching
:boolean - 数据是否正在加载,不管第几次加载,就会设置其状态 -
isLoading
:boolean - 表示数据是否第一次加载,通过调用refetch
不会改变此状态 -
isSuccess
:boolean - 请求是否成功 -
isUninitialized
:boolean -请求是否还没有开始发送,常用在删除操作的请求上 -
error
:对象 有错才存在对象,无错误就没有这个 -
data
:最新返回的数据(在重新发送请求的时候,会保存上一次请求返回的数据) -
currentData
:当前参数的最新数据,(当参数变化的时候,会清空(变为undefined))- 比如在一个列表搜索想要的汉堡名称,搜索的时候我如果想清空原来的数据并展示等待图标,数据加载完成后才渲染返回的数据,就可以使用
currentData
,如果不想清空原来的数据,而是等到数据返回后替换原来的数据,就可以使用data
- 比如在一个列表搜索想要的汉堡名称,搜索的时候我如果想清空原来的数据并展示等待图标,数据加载完成后才渲染返回的数据,就可以使用
useQuery设置参数
- useQuery第一个参数可以成为每一个请求的query函数传入的参数
//比如传入id
import {useGetStudentInfoQuery} from "../store/studentApi";
const {data,isSuccess} = useGetStudentInfoQuery(id);//异步请求
//则我们可以在api当中接收
const studentApi = createApi({
//...
endpoints:(build) => {
getStudentInfo:build.query({
query(id){
return `students/${id}`
}
j
})
}
//...
})
-
useQuery第二个参数可以传入对象通过该对象可以对请求进行配置
- 创建的时候传入的对api的配置可以说默认配置
- 使用的时候传入的第二个参数配置可以更加具有定制化特点,和默认配置发生冲突的时候以这个为准
-
selectFromResult
函数,返回值作为useQuery返回值- 比如我可以设置data的数据,对data数据进行过滤
const res = useGetStudentsQuery(null,{ selectFromResult: (result) => { if(result.data){ result.data = result.data.filter(item => item.age < 18) } return result; } });
-
pollingInterval
默认为0,设置轮询的间隔(隔一段时间发送请求),单位毫秒,为0表示不轮询 -
skip
设置是否跳过当前请求,默认false- 比如一个组件既有编辑功能,也有添加功能,那么我们需要在添加功能的时候跳过请求,否则就添加的时候就会向后台请求初始化数据
//当有id的时候,才请求数据,否则不请求 const {data,isSuccess} = useGetStudentInfoQuery(props.id,{ skip:!props.id, });//异步请求
-
refetchOnMountOrArgChange
:默认false 设置是否每次都重新加载数据(也就是设置是否不使用缓存)也可以设置为数字,为数字的话就是设置缓存有效期 -
refetchOnFocus
:默认false, 是否在重新获取焦点时重载数据(比如切换页面)- 如果设置为true,需要在store当中设置
setupListeners(store.dispatch)
import {setupListeners} from "@reduxjs/toolkit/query"
- 如果设置为true,需要在store当中设置
-
refetchOnReConnect
:默认false, 是否在重新连接后重载数据(没网了,重新连接了网)
RTKQ构造器构造API请求
- 如果发送的不是get信息(当然,get也可以设置配置对象),我们就不可能像之前写query一样了
// 之前的写法
const studentAPi = createApi({
//...
endpoints:(build) => {
return {
getStudent:build.query({
query(){
return 'students'
}
})
}
}
//...
})
//如果现在是put或者delete或者post,就需要返回配置对象的形式了
//并且更改构建器为mutation
const studentApi = createApi({
//...
endpoints:(build) => {
return {
delStudent:build.mutation({
query(参数){
return {
url:'students/${参数}',
method:'delete',
//如果有参数,则需要添加body
body:传递的数据
}
}
})
}
}
//...
})
- 删除的调用和查询的调用不太一样,因为删除不是页面加载后就立马删除,而是用户确认删除后才执行删除操作,添加的操作也是如此,修改的也是
//引入删除的接口
import {useDelStudentInfoMutation} from "../store/studentApi";
//输出查看内容
const a = useDelStudentInfoMutation();
//打印的内容
[f,{isError,isLoading,isSuccess,isUninitialized,originalArgs,reset,status}];
//第一个为触发器,第二个是结果集
//所以具体中我们可以获取触发器,在需要的时候调用就会执行函数
//调用的结果和状态和可以从结果集获取
const [delStudent,{isSuccess}] = useDelStudentInfoMutation();
//点击删除学生,正常的操作应该要询问用户是否删除的
const handleClick = useCallback(async (id) => {
delStudent(id);
})
jsx的具体操作添加,修改
const [formData,setFormData] = useState({
name: props.name ? props.name : "",
sex: props.sex ? props.sex : "男",
age: props.age ? props.age : "",
address: props.address ? props.address : "",
});
//修改数据
const [editStudentInfo,{isSuccess:editSuccess}] = useEditStudentInfoMutation();
//添加数据
const [addStudent] = useAddStudentInfoMutation();
/*添加回调确认*/
const handleAdd = useCallback(async () => {
//执行添加数据
addStudent({
...formData,
});
//清空数据
setFormData({
name: "",
sex: "男",
age: "",
address: "",
})
});
/*修改回调确认*/
const handleEditConfirm = useCallback(async () => {
//执行修改数据
editStudentInfo({
id:props.id,
info:formData,
});
},)
studentApi.js的query定义
//修改数据
editStudentInfo:build.mutation({
query(newInfo) {
return {
url:`students/${newInfo.id}`,
method:'put',
body:newInfo.info,
}
}
}),
//添加数据
addStudentInfo:build.mutation({
query(info){
return {
url:'students',
method:'post',
body:info,
}
}
})
RTKQ的数据标签
-
给API切片请求的数据打上标签,当标签失效的时候,就会重新加载标签对应的请求
-
可以看看简述这位人写的@知行合一_脚踏实地地-RTKQ中,给Api切片请求的数据打上标签
- 首先我们需要创建标签
const studentApi = createApi({
reducerPath:'xxxxx',
//用来指定API中的标签类型
tagTypes:['student'],
endpoints:(buidl) => {
//....
}
})
-
给请求的数据添加标签
- 给API切片的钩子函数添加
providesTags
属性,属性值可以为数组字符串,数组对象,回调函数
const studentApi = createApi({ tagTypes:['student'], endpoints:(build) => { return { xxxxx:build.query({ query(){ return xxxx }, providesTags:xxxxx }) } } })
- 当
providesTags
为数组字符串的时候- 等同于数组对象的简写
- 只要type和
invalidates
对应,就会重新请求数据
providesTags:['student']; //等同于 providesTags:[{type:'student'}]
- 当
providesTags
为数组对象的时候- 所有数据(type和id)都需要和
invalidates
对应才会重新请求数据 - id如果是字符串数字,如果有对应的id,依旧会失效
- 所有数据(type和id)都需要和
providesTags:[{type:'student',id:100}]; providesTags:[{type:'student',id:'100'}];
- 当
providesTags
为回调函数的时候
当providesTags属性的值为回调函数时,可以对标签的生效范围做更细致的划分 参数1:网络请求的返回结果 参数2:错误信息 参数3:钩子函数中传入的实参 //返回值为一个数组,符合数组元素条件的数据将生效 providesTags:(result,error,params,meta) => { return [{type:'student',id:params.id}] }
- 给API切片的钩子函数添加
-
给请求的数据设置要失效的标签
- 通过
invalidatesTags
进行设置
const studentApi = createApi({ tagTypes:['student'], endpoints:(build) => { return { delStudent:build.mutation({ query(id){ return { url:xxxx, method:'delete', } invalidatesTags:xxxx } }) } } })
- 当
invalidatesTags
为数组字符串时候- 只要
providesTags
当中type包含在invalidatesTags
,就会重新请求数据 - 数组字符串的写法等同于数组对象的简写
- 只要
invalidatesTags:['student']; providesTags:['student'];//让其失效 providesTags:[{type:'student'}];//让其失效 providesTags:[{type:'student',id:100}];//让其失效 invalidatesTags:['student']; 等同于,二个效果是一样的 invalidatesTags:[{type:'student'}];
- 当
invalidatesTags
为数组对象的时候- 指明id则让type和id二者都对应的标签失效
- 未指明就和数组字符串失效规则一样
invalidates:[{type:'student',id:10}]; providesTags:['student'];//不会失效 providesTags:[{type:'student'}];//不会失效 providesTags:[{type:'student',id:10}];//会失效 //实测会 providesTags:[{type:'student',id:'10'}];//会失效 providesTags:[{type:'student',id:888}];//不会失效
- 当
invalidatesTags
为回调函数的时候
invalidatesTags:(result,error,stu,meta) => { return [ {type:'student',id:stu.id}, {type:'student',id:'LIST'} ] }
- 通过
- 示例,老师的例子
- 当添加数据后,会刷新列表
- 当编辑后,会刷新列表,但是如果数据没有变动的话,点击编辑就不会重新查询(存在缓存的前提下)
import {createApi, fetchBaseQuery} from "@reduxjs/toolkit/dist/query/react";
//创建Api对象
//createApi()用来创建RTKQ中的API对象
const studentApi = createApi({
reducerPath:'studentApi',//Api的标识,不能和其他Api或reducer重复
//指定查询的基础信息,发送请求使用的工具
baseQuery:fetchBaseQuery({
baseUrl:'http://localhost:3000/'
}),
//用来指定api当中的标签
tagTypes:['student'],
endpoints:(build) => {
//build是请求的构建器,通过build来设置请求相关的信息
return {
getStudents:build.query({
query() {
//用来指定请求的子路径
return 'students';
},
providesTags:[{type:'student',id:'initList'}]
}),
//编辑的时候获取信息
getStudentInfo:build.query({
query(id) {
return `students/${id}`;
},
providesTags:(result, error, arg, meta) => {
return [
{type:'student',id:arg},
]
},
设置缓存时间为5秒
//keepUnusedDataFor:5,
}),
//删除数据
delStudentInfo:build.mutation({
query(id){
return {
url:`students/${id}`,
method:'delete',
}
},
invalidatesTags:[{type:'student',id:'initList'}]
}),
//修改数据
editStudentInfo:build.mutation({
query(newInfo) {
return {
url:`students/${newInfo.id}`,
method:'put',
body:newInfo.info,
}
},
//修改数据的时候,只针对修改过的列表进行重新查询数据,未改变的不变动
invalidatesTags:(result, error, arg, meta) => {
return [
{type:'student',id:'initList'},
{type:'student',id:arg.id},
]
}
}),
//添加数据
addStudentInfo:build.mutation({
query(info){
return {
url:'students',
method:'post',
body:info,
}
},
//添加数据的时候重新刷新列表
invalidatesTags:[{type:'student',id:'initList'}]
})
}
}
})
//api对象创建后,对象中会根据各种方法自动的生成对应的钩子函数
//通过这些钩子函数,可以向服务器发送请求
//钩子函数的命名规则 endpoints中有一个getStudents 结合查询方式=>生成 useGetStudentsQuery
export const { useGetStudentsQuery,
useGetStudentInfoQuery,
useDelStudentInfoMutation,
useAddStudentInfoMutation,
useEditStudentInfoMutation,
} = studentApi;
//我们还需要使用store
export default studentApi;
RTKQ使用axios
- 其实使用很简单,就是axios的二次封装
- 安装
npm install axios
- 更改
import {createApi} from "@reduxjs/toolkit/dist/query/react";
import axios from "axios";
const studentApi = createApi({
reducerPath:'studentApi',//Api的标识,不能和其他Api或reducer重复
//指定查询的基础信息,发送请求使用的工具
//baseQuery:fetchBaseQuery({
// baseUrl:'http://localhost:3000/'
//}),
//更改为axios
baseQuery:axios.create({
baseURL:'http://localhost:3000/'
}),
})
-
请求体变更
- 比如post或者put等传参需要使用data,而不是body了
//修改数据 editStudentInfo:build.mutation({ query(newInfo) { return { url:`students/${newInfo.id}`, method:'put', //之前fetch的时候 //body:newInfo.info data:newInfo.info, } }, //修改数据的时候,只针对修改过的列表进行重新查询数据,未改变的不变动 invalidatesTags:(result, error, arg, meta) => { return [ {type:'student',id:'initList'}, {type:'student',id:arg.id}, ] } }),
react-router-dom@5
- 安装
yarn add react-router-dom@5
- 使用
- 技巧 ,可以为路由模式起别名,当我们更改的时候,就不需要去组件更改了
index.js主入口
import ReactDOM from "react-dom/client"
import App from "./App"
import {BrowserRouter as Router} from "react-router-dom";
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<Router>
<App/>
</Router>
)
App.jsx
- 使用Route对路径进行注册
const App = () => {
return (
<div>
<Link to="/">主页</span>
<Link to="/about">关于</span>
<Route path={'/'} component={Home}></Route>
<Route path={'/about'} component={About}></Route>
</div>
);
};
export default App;
- 注意
- 默认情况下Route并不是严格匹配,只要url地址的头部和path一致,组件就会挂载,不会检查子路径(也就是说url地址中包含了path,就会挂载组件)
- 所以上述代码会有问题,当我们进入
/about
的时候,即会加载Home
组件和About
,因为使用的是模糊匹配,所以我们可以使用精准匹配
<Route exact path={'/'} component={Home}></Route>
<Route exact path={'/about'} component={About}></Route>
NavLink
-
特殊版本的Link,可以根据不同的情况设置不同的样式。
-
属性:
- activeClassName —— 字符串 链接激活时的class
- activeStyle —— 对象 链接激活时的样式
- isActive —— 函数,可动态判断链接是否激活
- style —— 函数,动态设置样式
- className —— 函数,动态设置class值
Route的传递方式
- 方法1 — 通过component
import Home from "./Home";
<Route path="/student/:id" component={Home}/>
- 方法2-通过render
- render需要传入一个回调函数,回调函数有一个routeProps参数,返回值为要渲染的组件
import Home from "./Home";
//这样子就可以传递数据了,但是不会自动传递match,location,history了
<Route path="/student/:id" render = { () => <Home/> } />
//如果需要match.location,history
<Route path="/student/:id" render={(routeProps) => <Home {...routeProps}/>} />
-
方法3-通过children来指定被挂载的组件
- 用法1:children设置一个回调函数时候和render类似,写法一样可以说,当,该组件无论路径是否匹配都会挂载
<Route path="/student/:id" children={(routeProps) => <Home {...routeProps}/>}/> //浏览器 访问/ 加载home 访问/abc 加载home
-
用法2:传入一个jsx,路径匹配才加载
- 当是无法传入match,location,history;但是可以用钩子函数解决
const match = useRouteMatch() const location = useLocation(); const history = useHistory(); const params = useParmas();//获取params参数
<Route path="/student/:id" children={ <Home}/>}/>
-
方法4-通过prop.children来
- 依据是当路径不匹配依旧会挂载
<Route path="/student/:id">
<Home/>
</Route>
//如果需要match location history
<Route path="/student/:id">
{
routeProps => <Home {...routeProps}/>
}
</Route>
- 传递的三大属性
match:{
isExact 检查路径是否完全匹配
params: {} (默认空对象)请求的参数
path:设置的path属性 比如/student /student/:id
url: 真实的路径比如/student /student/4
} 匹配的信息
location: {
has:
key:请求的id
search:(默认undefined)查询的字符串比如/student?name=admin
state:(默认undefined)
}地址信息
history: {
go自由跳转(方法)
goBack向后条(方法)
goForwar向前跳(方法)
push:历史记录添加一条新页面并跳转(方法)
replace:替换记录并跳转(方法),跳转可以传递state
replace({pathname:'/student/2,state:{name:李白},)
}控制页面的跳转
路由的嵌套
<Route path="/about">
<About/>
<Route path="/about/hello">
<Hello/>
</Route>
</Route>
Prompt组件
- 跳转确认
- 比如表单输入了,然后用户想跳转到另外一个页面,此时我们就可以使用
Prompt
组件,询问用户是否跳转 message
属性设置提示信息when
属性当为true的时候才会进行循环,默认值是true
- 下面示例,当input有值的时候,就会进行询问用户是否进行跳转
import React, {useState} from 'react';
import {Prompt} from "react-router-dom"
const MyForm = () => {
const [value,setValue] = useState('');
const [isPrompt,setPrompt] = useState(false);
const handleOnChange = (e) => {
setValue(e.target.value);
setPrompt(!!e.target.value.trim().length);
}
return (
<div>
<Prompt message={'确定离开当前页面吗'} when={isPrompt}/>
<p>form组件</p>
<input type='text' value={value} onChange={handleOnChange}/>
</div>
);
};
export default MyForm;
Redirect
-
重定向功能
-
属性
to
from
replace
(默认替换方式为replace)push
//当访问abc的时候,自动重定向到form <Redirect from = "/abc" to="/form"/>
-
如果有
Switch
,可以实现重定向功能
<Switch>
<Route path="/about" component={About}/>
<Route path="/home" component={Home}/>
//当上面的都匹配不到的时候,就会匹配Redirect组件当中的to值
<Redirect to="/home"/>
</Switch>
react-router-dom@6
-
在react路由@6版本当中,我们必须要在所有的
Route
外面包一层Routes
,作用和Switch
类型,Routes
中的Route
只有一个会被匹配 -
Route
不再使用component
属性,而是element
属性 -
NavLink
-
useParams
,useLocation
没有变化 -
useMatch
检查当前url是否匹配某个路由(注意,是路由,不是路径,比如路由是/student/:id
,而不是/student/1
)- 匹配:返回一个对象
- 不匹配返回null
-
useHistory
删除,使用useNavigate
代替,获取用于跳转页面的函数const nav = useNavigate();nav('/home')跳转到/home
默认push方法nav('/about',{replace:true})
,使用replace跳转
-
Navigate
标签添加
传递params
useParams
可以获取params参数
import React from 'react';
import {useParams} from "react-router-dom";
const STATIC_DATA = [
{id:1,name:'傻瓜超人'},
{id:2,name:'西瓜超人'},
{id:3,name:'动感超人'},
{id:4,name:'酸梅超人'},
]
const About = () => {
const {id} = useParams();
//不要忘记转化为了数字
const findData = STATIC_DATA.find(item => item.id === id*1)
console.log(findData)
return (
<div>
<h2>超人的类型</h2>
<h3>{findData.id} --- {findData.name}</h3>
</div>
);
};
export default About;
useNavigate(钩子)
- 可以获取一个用于页面跳转的函数
import React from 'react';
import {useNavigate} from "react-router-dom"
const Home = () => {
const router = useNavigate()
const handleClick = () => {
//使用replace模式(默认不设置是push模式)
router('/about/1',{
replace:true,
})
}
return (
<div>
我是精彩的主页
<button onClick={handleClick}>跳转到about页面</button>
</div>
);
};
export default Home;
路由的嵌套和Outlt占位
<Routes>
<Route path={'/home'} element={<Home/>}>
<Route path={'page'} element={<HomePage/>}/>
</Route>
<Route path={'/about/:id'} element={<About/>}> </Route>
</Routes>
Home.jsx使用Outlet占位
import React from 'react';
import {useNavigate,Outlet} from "react-router-dom"
const Home = () => {
const router = useNavigate()
const handleClick = () => {
//使用replace模式(默认不设置是push模式)
router('/about/1',{
replace:true,
})
}
return (
<div>
我是精彩的主页
<button onClick={handleClick}>跳转到about页面</button>
<Outlet/>
</div>
);
};
export default Home;
Outlet
表示嵌套路由的组件,当嵌套路由中的路由匹配成功了,Outlet则表示嵌套路由的组件,没有匹配成功的话,Outlet就什么都不是
Navigate组件
-
Navigate只要被渲染出来,就会引起视图的切换
-
replace
属性用于控制跳转模式(push 或 replace,默认是push)
NavLink
- className的方式
//css内容
.active_style{
background-color: red;
}
//jsx内容
import React from 'react';
import {Link,NavLink} from "react-router-dom";
import "./test.css";
const Menu = () => {
const activeStyle = ({isActive}) => {
return isActive ? 'active_style' : null;
}
return (
<div>
<Link to={'/home'}>Home页面</Link>
{/*<NavLink to='/home/page' className={activeStyle}>Home页面下的page</NavLink>*/}
{/*或者*/}
<NavLink to={'/home/page'} className={
({isActive}) => {
return isActive ? 'active_style' : null;
}
}>Home页面下的page</NavLink>
<Link to={'/about'}>关于页面</Link>
</div>
);
};
export default Menu;
//激活的时候HTML状态
<a href="/home/page" aria-current="page" class="active_style">Home页面下的page</a>
//未激活的时候HTML状态
<a href="/home/page">Home页面下的page</a>
- style的方式
import React from 'react';
import {Link,NavLink} from "react-router-dom";
const Menu = () => {
const activeStyle = ({isActive}) => {
return isActive ? {backgroundColor:'red'} : null;
}
return (
<div>
<Link to={'/home'}>Home页面</Link>
{/*<NavLink to='/home/page' style={*/}
{/* ({isActive}) => {*/}
{/* return isActive ? {backgroundColor:'red'} : null*/}
{/* }*/}
{/*}>Home页面下的page</NavLink>*/}
<NavLink to='/home/page' style={activeStyle}>Home页面下的page</NavLink>
<Link to={'/about'}>关于页面</Link>
</div>
);
};
export default Menu;
//激活的时候HTML状态
<a aria-current="page" class="active" href="/home/page" style="background-color: red;">Home页面下的page</a>
//未激活的时候HTML状态
<a class="" href="/home/page" style="">Home页面下的page</a>
其他钩子
useMemo
- 可以缓存一切,类似于useEffect,useCallback
- useCallback用来缓存函数对象,useMemo用来缓存函数的执行结果
React.forwardRef和useImperativeHandle
-
React.forwardRef可以暴露ref对象给外部(比如可以在A组件通过ref来操作B组件当中的input输入框(完整的一个DOM))
-
而useImplerativeHandle可以指定暴露的ref对象给外部(比如A组件可以通过ref来操作B组件当中的input输入框的值(只可以操作这个输入框的值))
-
我们先来看看,如果直接给一个自定义组件绑定
ref
会发生什么- 结果很明显,React提示
Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
,同时,输出ref也无法获取到值多个dom对象,react也不知道要给你谁 - 因为无法直接去获取react组件的dom对象,因为一个react组件可以含有
- 结果很明显,React提示
App.jsx组件
import React,{useState,useRef} from 'react';
import Some from "./component/Some";
const App = () => {
const [count,setCount] = useState(0);
const someRef = useRef();
const handleShow = () => {
console.log(someRef.current)
}
return (
<div>
<p>总数:{count}</p>
<button onClick={() => setCount(prevState => prevState + 1)}>点击加1</button>
<button onClick={handleShow}>查看Some的ref的值</button>
<Some ref={someRef}/>
</div>
);
};
export default App;
Some.jsx组件
import React,{useRef} from 'react';
const Some = () => {
const inputRef = useRef();
const handleBtn = () => {
console.log(inputRef.current.value);
}
return (
<div>
我是Some组件
<input ref={inputRef}/>
<button onClick={handleBtn}>获取input的值</button>
</div>
);
};
export default Some;
- 所以我们需要使用
React.forwardRef
在自定义组件Some当中
Some.jsx组件
import React,{useRef,forwardRef} from 'react';
const Some = forwardRef((props,ref) => {
const inputRef = useRef();
const handleBtn = () => {
console.log(inputRef.current.value);
}
return (
<div>
<p ref={ref}>我是Some组件</p>
<input ref={inputRef}/>
<button onClick={handleBtn}>获取input的值</button>
</div>
);
})
export default Some;
App.jsx组件
import React,{useState,useRef} from 'react';
import Some from "./component/Some";
const App = () => {
const [count,setCount] = useState(0);
const someRef = useRef();
const handleShow = () => {
console.log(someRef)
}
return (
<div>
<p>总数:{count}</p>
<button onClick={() => setCount(prevState => prevState + 1)}>点击加1</button>
<button onClick={handleShow}>查看Some的ref的值</button>
<Some ref={someRef}/>
</div>
);
};
export default App;
- 但是这样子使用
forwardRef
很不安全,因为你在外部组件直接可以操控这个Some组件的input,完完全全拿到了input,相当于你为了量一块钻石的尺寸,把这个钻石送给了比如,所以我们可以使用useImperativeHandle
,只把"钻石"的一些属性告诉他
App.jsx
import React,{useState,useRef} from 'react';
import Some from "./component/Some";
const App = () => {
const [count,setCount] = useState(0);
const someRef = useRef();
const handleShow = () => {
const temp = count + 1;
setCount(temp);
/*设置Some组件当中的p标签的内容*/
someRef.current.setContext(temp)
}
return (
<div>
<p>总数:{count}</p>
<button onClick={handleShow}>点击加1并设置Some组件P的值</button>
<Some ref={someRef}/>
</div>
);
};
export default App;
Some.jsx
import React,{useRef,forwardRef,useImperativeHandle} from 'react';
const Some = forwardRef((props,ref) => {
const inputRef = useRef();
const pRef = useRef()
const handleBtn = () => {
console.log(inputRef.current.value);
}
useImperativeHandle(ref,() => {
/*返回值将作为App.jsx组件获取到的ref的值*/
return {
setContext(content){
pRef.current.textContent = content;
}
}
})
return (
<div>
<p ref={pRef}>我是Some组件</p>
<input ref={inputRef}/>
<button onClick={handleBtn}>获取input的值</button>
</div>
);
})
export default Some;
效果图
useEffect和useInsertionEffect和useLayoutEffect
- 执行顺序从先到后
useInsertionEffect
>useLayoutEffect
>useEffect
- 当使用
useEffect
会出现闪的情况的时候,再考虑用其他二个
useDeferredValue
- 当我们多个组件使用同一个state时,组件有可能会互相影响,一个组件卡顿,会导致所有组件都卡
碎笔记
-
组件首字母必须要大写
-
react想要取消默认行为,可以通过事件对象来,比如event.preventDefault()取消默认行为,event.stopPropagation()取消事件冒泡
-
props是只读的,不能修改props属性
-
toLocaleString()
- 没想到这么多可以使用这个方法
-
rsc-函数组件不带props
-
rsi函数组件带props
-
rcc-类组件
-
props.children表示函数的标签体
-
React中的钩子函数只能在函数组件或在自定义钩子中调用(是直接在函数组件使用
function App() {
function fn(){
usexxxxx,//错误,这不叫函数组件调用,而是在函数组件的内部函数调用了,错误
}
}
- 在类中直接定义的箭头函数,this永远都指向实例对象
class MyClass {
fn = () => {
//在类中直接定义的箭头函数,this永远都指向实例对象
}
fn2(){
const fn3 = () => {
//这都不是在类中直接定义的箭头函数了,肯定不是指向实例对象
}
}
}
- 为什么路由的时候不能使用超链接来实现路由的跳转?
- 会导致向服务器发送请求重新加载页面,并且如果服务器没有做跳转操作下,在
BrowserRouter
模式下会发生404的情况通过链接跳转的方式(因为这个请求没有经过react-router进行处理),所以为了避免这种情况有二种解决办法- 使用HashRouter 服务器不会处理#后面的东东,我们请求localhost/#/about,服务器只会处理localhost,不会处理/about
- 如果依旧需要使用BrowserRouter, 就需要修改服务器的配置,将所有请求都转发到index.html
- 会导致向服务器发送请求重新加载页面,并且如果服务器没有做跳转操作下,在
//错误写法
<a href="/">跳转到主页</a>
<a href="/about">跳转到关于页</a>
fetchBaseQuery
设置请求头并读取state的值
const studentApi = createApi({
// ...
baseQuery: fetchBaseQuery({
baseUrl: "http://localhost:1337/api",
prepareHeaders: (headers,abc) => {
console.log(abc);
//输出
endpoint: "getStudents"
extra: undefined
forced: false
getState: ƒ a()
type: "query"
return headers;
}
})
// ...
});
const studentApi = createApi({
// ...
baseQuery: fetchBaseQuery({
baseUrl: "http://localhost:1337/api",
prepareHeaders: (headers, {getState}) => {
const token = getState().auth.token;
return headers;
}
}),
// ...
});
- 钩子只能在React组件和自定义钩子中使用
- 钩子不能再嵌套函数或其他语句(if,switch,for)等中使用