【React自学笔记07】React18函数式组件必备知识点合集2
一、样式设置
1.内联样式
<div style={{color:'red'}}>
我是Div
</div>
样式过多时,可以把样式提取出来作为变量定义
import React from 'react';
const StyleDemo = () => {
const divStyle = {color: 'red', backgroundColor: '#bfa', fontSize: 20, borderRadius: 12}
return (
<div style={divStyle}>
我是Div
</div>
);
};
export default StyleDemo;
根据不同的state值应用不同的样式
import React, {useState} from 'react';
const StyleDemo = () => {
const [showBorder, setShowBorder] = useState(false);
const divStyle = {
color: 'red',
backgroundColor: '#bfa',
fontSize: 20,
borderRadius: 12,
border: showBorder?'2px red solid':'none'
};
const toggleBorderHandler = ()=> {
setShowBorder(prevState => !prevState);
};
return (
<div style={divStyle}>
我是Div
<button onClick={toggleBorderHandler}>切换边框</button>
</div>
);
};
export default StyleDemo;
2.外部样式表
...
3.模块化css
使用步骤:
-
创建一个xxx.module.css
-
在组件中引入css import classes from './App.module.css';
-
通过classes来设置类 className=
特点:
CSS模块可以动态的设置唯一的class值,react会对类名重命名,脚本自动生成
import React, {useState} from 'react';
import classes from './App.module.css';
import A from "./A";
const App = () => {
const [showBorder, setShowBorder] = useState(false);
const clickHandler = () => {
setShowBorder(true);
};
return (
<div>
<A/>
<p className={`${classes.p1} ${showBorder ? classes.Border : ''}`}>我是一个段落</p>
<button onClick={clickHandler}>点我一下</button>
</div>
);
};
export default App;
//App.module.css
.p1{
color: red;
background-color: #bfa;
}
.Border{
border: 1px red solid;
}
二、外部容器
React.Fragment
- 是一个专门用来作为父容器的组件
- 它只会将它里边的子元素直接返回,不会创建任何多余的元素
- 当我们希望有一个父容器,但同时又不希望父容器在网页中产生多余的结构时,就可以使用Fragment
//App.js
import React,{Fragment} from 'react'
const App=()=>{
return (
<Fragment>
<div>第一个组件</div>
<div>第二个组件</div>
<div>第三个组件</div>
</Fragment>
)
}
export default App;
import React from 'react';
const App = () => {
return (
<>
<div>第一个组件</div>
<div>第二个组件</div>
<div>第三个组件</div>
</>
);
};
export default App;
三、公共context
Context相当于一个公共的存储空间,我们可以将多个组件中都需要访问的数据统一存储到一个Context中,这样无需通过props逐层传递,即可使组件访问到这些数据。通过React.createContext()创建context
📌使用方式一:
- 1.引入context
- 2.使用 Xxx.Consumer 组件来创建元素
- Consumer 的标签体需要一个回调函数(组件中必须传一个函数作为参数)
- 它会将context设置为回调函数的参数,通过参数就可以访问到context中存储的数据
import React from 'react';
import TestContext from "../store/testContext";
const A = () => {
return (
<TestContext.Consumer>
{(ctx)=>{
return <div>
{ctx.name} - {ctx.age}
</div>
}}
</TestContext.Consumer>
);
};
export default A;
import React from 'react';
const TestContext = React.createContext({
name:'孙悟空',
age:18
});
export default TestContext;
📌使用方式二:
- 1.导入Context
- 2.使用钩子函数useContext()获取到context
- useContext() 需要一个Context作为参数,它会将Context中数据获取并作为返回值返回
Xxx.Provider
- 表示数据的生产者,可以使用它来指定Context中的数据
- 通过value来指定Context中存储的数据,这样一来,在该组件的所有的子组件中都可以通过Context来访问它所指定数据
- 当我们通过Context访问数据时,他会读取离他最近的Provider中的数据,如果没有Provider,则读取Context中的默认数据
import React, {useContext} from 'react';
import TestContext from "../store/testContext";
const B = () => {
// 使用钩子函数获取Context
const ctx = useContext(TestContext);
return (
<div>
{ctx.name} -- {ctx.age}
</div>
);
};
export default B;
四、Effect
React组件有部分逻辑都可以直接编写到组件的函数体中的,像是对数组调用filter、map等方法,像是判断某个组件是否显示等。但是有一部分逻辑如果直接写在函数体中,会影响到组件的渲染,这部分会产生“副作用”的代码,是一定不能直接写在函数体中。
例如,如果直接将修改state的逻辑编写到了组件之中,就会导致组件不断的循环渲染,直至调用次数过多内存溢出
当我们直接在函数体中调用setState时,就会触发Too many re-renders
📌 函数组件中setState的执行流程:
setCount() 调用 dispatchSetDate() 会先判断,组件当前处于什么阶段
- 如果是渲染阶段 --> 不会检查state值是否相同
- 如果不是渲染阶段 --> 会检查state的值是否相同
- 如果值不相同,则对组件进行重新渲染
- 如果值相同,则不对组件进行重新渲染
- 如果值相同,React在一些情况下会继续执行当前组件的渲染,但是这个渲染不会触发其子组件的渲染,这次渲染不会产生实际的效果,这种情况通常发生在值第一次相同时
如果熟悉 React class 的生命周期函数,你可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合。
componentDidMount 组件挂载
componentDidUpdate 组件更新
componentWillUnmount 组件将要摧毁
第二个参数存放变量,当数组存放变量发生改变时,第一个参数,逻辑处理函数将会被执行
第二个参数如果只传一个空数组,逻辑处理函数里面的逻辑只会在组件挂载时执行一次 ,不就是相当于 componentDidMount
在Effect的回调函数中,可以指定一个函数作为返回值,这个函数可以称其为清理函数,它会在下次Effect执行前调用,可以在这个函数中,做一些工作来清除上次Effect执行所带来的的影响
useEffect(()=>{
// 降低数据过滤的次数,提高用户体验
// 用户输入完了你在过滤,用户输入的过程中,不要过滤
// 当用户停止输入动作1秒后,我们才做查询
// 在开启一个定时器的同时,应该关掉上一次
const timer = setTimeout(()=>{
console.log('Effect触发了!');
props.onFilter(keyword);
}, 1000);
return () => {
clearTimeout(timer);
};
}, [keyword]);
五、Reducer整合器
理解:在一个组件中,把对同一state进行操作的函数整合到一起
useReducer(reducer, initialArg, init)
参数:
- reducer : 整合函数
- 对于我们当前state的所有操作都应该在该函数中定义
- 该函数的返回值,会成为state的新值
- reducer在执行时,会收到两个参数:
- state 当前最新的state
- action 它需要一个对象,在对象中会存储dispatch所发送的指令
- initialArg : state的初始值,作用和useState()中的值是一样
- init:初始函数(一般用的不是很多)
返回值(数组):
- 第一个参数,state 用来获取state的值
- 第二个参数,state 修改的dispatch派发器
- 通过派发器可以发送操作state的命令
- 具体的修改行为将会由另外一个函数(reducer)执行
import React, {useReducer, useState} from 'react';
// 为了避免reducer会重复创建,通常reducer会定义到组件的外部
const countReducer = (state, action) => {
// 可以根据action中不同type来执行不同的操作
switch (action.type) {
case 'ADD':
return state + 1;
case 'SUB':
return state - 1;
default:
return state;
}
};
const App = () => {
// useReducer(reducer, initialArg, init)
const [count, countDispatch] = useReducer(countReducer, 1);
const addHandler = () => {
// 增加count的值
countDispatch({type: 'ADD'});
};
const subHandler = () => {
// 增加count的值
countDispatch({type: 'SUB'});
};
return (
<div>
<button onClick={subHandler}>减少</button>
{count}
<button onClick={addHandler}>增加</button>
</div>
);
}
;
export default App;
六、重新渲染
重新渲染的含义:触发了组件中的代码全部重新执行一遍
React组件发生重新渲染的条件
- 第一种,当组件自身的state发生变化时
- 第二种,当组件的父组件重新渲染时
React.memo
React.memo() 是一个高阶组件,对函数式组件进行缓存。它接收另一个组件作为参数,并且会返回一个包装过的新组件。包装过的新组件就会具有缓存功能,包装过后,只有组件的props发生变化,才会触发组件的重新的渲染,否则总是返回缓存中结果,避免对组件的重新渲染
React.memo()该方法是一个高阶函数,可以用来根据组件的props对组件进行缓存,当一个组件的父组件发生重新渲染,而子组件的props没有发生变化时,它会直接将缓存中的组件渲染结果返回而不是再次触发子组件的重新渲染,这样一来就大大的降低了子组件重新渲染的次数。
//App.js
import React, {useState} from 'react';
import A from "./components/A";
const App = () => {
console.log('App渲染');
const [count, setCount] = useState(1);
const clickHandler = () => {
setCount(prevState => prevState + 1);
};
return (
<div>
<h2>App -- {count}</h2>
<button onClick={clickHandler}>增加</button>
<A/>
</div>
);
};
export default App;
//A.js
import React, {useState} from 'react';
import B from "./B";
const A = () => {
console.log('A渲染');
const [count, setCount] = useState(1);
const clickHandler = () => {
setCount(prevState => prevState + 1);
};
const test = count % 4 === 0;
return (
<div>
<h2>组件A -- {count}</h2>
<button onClick={clickHandler}>增加</button>
<B test={test}/>
</div>
);
};
export default React.memo(A);
//B.js
import React from 'react';
const B = (props) => {
console.log('B渲染');
return (
<div>
<h2>组件B</h2>
<p>{props.test && '哈哈'}</p>
</div>
);
};
export default React.memo(B);//props不发生变化时,避免组件B重新渲染
useCallback()
useCallback 是一个钩子函数,用来创建React中的回调函数
对函数进行缓存
useCallback()参数:
-
回调函数
- useCallback 创建的回调函数不会总在组件重新渲染时重新创建
-
依赖数组
- 当依赖数组中的变量发生变化时,回调函数才会重新创建
- 如果不指定依赖数组,回调函数每次都会重新创建
- 依赖数组为空,在初始化时创建一次,其他情况,state改变,组件重新刷新,useCallback里的内容不会改变
- 一定要将回调函数中使用到的所有变量都设置到依赖数组中 除了(setState)
//App.js
import React, {useCallback, useState} from 'react';
import A from "./components/A";
const App = () => {
console.log('App渲染');
const [count, setCount] = useState(1);
// const clickHandler = () => {
// setCount(prevState => prevState + 1);
// };
const [num, setNum] = useState(1);
const clickHandler = useCallback(() => {
setCount(prevState => prevState + num);
setNum(prevState => num + 1);
}, [num]);
return (
<div>
<h2>App -- {count}</h2>
<button onClick={clickHandler}>增加</button>
<A onAdd={clickHandler}/>
</div>
);
};
export default App;
七、strapi
1. 安装
安装代码
npx create-strapi-app@latest server --quickstart
yarn create strapi-app server --quickstart [server是新建的strapi项目名] (个人推荐)
问题处理:
📌对于安装过程速度太慢,解决报错问题:info There appears to be trouble with your network connection. Retrying...
更换安装依赖的镜像,使用淘宝镜像安装
yarn config set registry https://registry.npm.taobao.org
📌strapi官方推荐版本:node:v14.x npm:v6.x
如何通过nvm管理安装和卸载node,以及管理node版本
https://www.jianshu.com/p/7e2f9e465ffc
2. 使用
npm run develop
创建字段
添加值
开放接口
八、fetch
在effect中加载数据,fetch() 用来向服务器发送请求加载数据,是Ajax的升级版,它需要两个参数:1.请求地址 2.请求信息
将写死的数据替换为从接口 http://localhost:1337/api/students中加载的数据,组件初始化时需要向服务器发送请求来加载数据
1显示学生
添加数据加载的提示、处理错误
import React, { useEffect, useState } from 'react';
import StudentList from "./components/StudentList";
import './App.css';
const App = () => {
const [stuData, setStuData] = useState([]);
// 添加一个state来记录数据是否正在加载,false表示没有加载数据,true表示加载
const [loading, setLoading] = useState(false);
// 创建一个state来记录错误信息
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);//设置loading为true
setError(null);// 重置错误
fetch('http://localhost:1337/api/students')
.then((res) => {
// 判断是否正常返回响应信息
if(res.ok){
// response表示响应信息
return res.json();// 该方法可以将响应的json直接转换为js对象
}
// 抛出一个错误
throw new Error('数据加载失败!');
})
.then(data => {
// 将加载到的数据设置到state中
setStuData(data.data);
// 数据加载完毕设置loading为false
setLoading(false);
})
.catch((e) => {
// catch中的回调函数,用来统一处理错误
setLoading(false);
// 设置错误状态
setError(e);
});
}, []);
return (
<div className="app">
{(!loading && !error) && <StudentList stus={stuData}/>}
{loading && <p>数据正在加载中...</p>}
{error && <p>数据加载异常!</p>}
</div>
);
};
export default App;
await版本
useEffect(()=>{
const fetchData = async () => {
try{
setLoading(true);
setError(null);
const res = await fetch('http://localhost:1337/api/students');
if(res.ok){
const data = await res.json();
setStuData(data.data);
}else{
throw new Error('数据加载失败!');
}
}catch (e){
setError(e);
}finally {
setLoading(false);
}
};
fetchData();
}, []);
2删除学生
要点:context的使用
//StuContext.js
import React from "react";
const StuContext = React.createContext({
fetchData: () => {}
});
export default StuContext;
//App.js
import React, {useCallback, useEffect, useState} from 'react';
import StudentList from "./components/StudentList";
import './App.css';
import StuContext from "./store/StuContext";
const App = () => {
const [stuData, setStuData] = useState([]);
// 添加一个state来记录数据是否正在加载,false表示没有加载数据,true表示加载
const [loading, setLoading] = useState(false);
// 创建一个state来记录错误信息
const [error, setError] = useState(null);
const fetchData = useCallback(async () => {
try {
setLoading(true);
setError(null);
const res = await fetch('http://localhost:1337/api/students');
//判断请求是否加载成功
if (res.ok) {
const data = await res.json();
setStuData(data.data);
} else {
throw new Error('数据加载失败!');
}
} catch (e) {
setError(e);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchData();
}, []);
const loadDataHandler = () => {
fetchData();
};
return (
<StuContext.Provider value={{fetchData}}>
<div className="app">
<button onClick={loadDataHandler}>加载数据</button>
{(!loading && !error) && <StudentList stus={stuData}/>}
{loading && <p>数据正在加载中...</p>}
{error && <p>数据加载异常!</p>}
</div>
</StuContext.Provider>
);
};
export default App;
//Student.js
import React, {useCallback, useContext, useState} from 'react';
import StuContext from "../store/StuContext";
const Student = (props) => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const ctx = useContext(StuContext);
const delStu = useCallback(async ()=>{
try{
setLoading(true);
setError(null);
const res = await fetch(`http://localhost:1337/api/students/${props.stu.id}`,{
method:'delete'
});
// 判断是否成功
if(!res.ok){
throw new Error('删除失败!');
}
// 修改成功后,需要触发列表刷新
ctx.fetchData();
}catch (e){
setError(e);
}finally {
setLoading(false);
}
},[]);
const deleteHandler = () => {
delStu();
};
return (
<>
<tr>
<td>{props.stu.attributes.name}</td>
<td>{props.stu.attributes.gender}</td>
<td>{props.stu.attributes.age}</td>
<td>{props.stu.attributes.address}</td>
<td>
<button onClick={deleteHandler}>删除</button>
</td>
</tr>
{loading && <tr>
<td colSpan={5}>正在删除数据...</td>
</tr>}
{error && <tr>
<td colSpan={5}>删除失败...</td>
</tr>}
</>
);
};
export default Student;
3添加学生
要点:数据双向绑定、callback、发送post请求
上述所有功能整合代码如下:
//App.js
import React, {useCallback, useEffect, useState} from 'react';
import StudentList from "./components/StudentList";
import './App.css';
import StuContext from "./store/StuContext";
const App = () => {
const [stuData, setStuData] = useState([]);
// 添加一个state来记录数据是否正在加载,false表示没有加载数据,true表示加载
const [loading, setLoading] = useState(false);
// 创建一个state来记录错误信息
const [error, setError] = useState(null);
const fetchData = useCallback(async () => {
try {
setLoading(true);
setError(null);
const res = await fetch('http://localhost:1337/api/students');
//判断请求是否加载成功
if (res.ok) {
const data = await res.json();
setStuData(data.data);
} else {
throw new Error('数据加载失败!');
}
} catch (e) {
setError(e);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchData();
}, []);
const loadDataHandler = () => {
fetchData();
};
return (
<StuContext.Provider value={{fetchData}}>
<div className="app">
<button onClick={loadDataHandler}>加载数据</button>
{(!loading && !error) && <StudentList stus={stuData}/>}
{loading && <p>数据正在加载中...</p>}
{error && <p>数据加载异常!</p>}
</div>
</StuContext.Provider>
);
};
export default App;
//StuContext
import React from "react";
const StuContext = React.createContext({
fetchData: () => {
}
});
export default StuContext;
//StudentList.js
import React from 'react';
import Student from "./Student";
import './StudentList.css';
import StudentForm from "./StudentForm";
const StudentList = (props) => {
return (
<table>
<caption>学生列表</caption>
<thead>
<tr>
<th>姓名</th>
<th>性别</th>
<th>年龄</th>
<th>地址</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{props.stus.map(stu => <Student key={stu.id} stu={stu}/> )}
</tbody>
<tfoot>
<StudentForm/>
</tfoot>
</table>
);
};
export default StudentList;
//Student.js
import React, {useCallback, useContext, useState} from 'react';
import StuContext from "../store/StuContext";
import StudentForm from "./StudentForm";
const Student = (props) => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [isEdit, setIsEdit] = useState(false);
const ctx = useContext(StuContext);
const delStu = useCallback(async () => {
try {
setLoading(true);
setError(null);
const res = await fetch(`http://localhost:1337/api/students/${props.stu.id}`, {
method: 'delete'
});
if (!res.ok) {
throw new Error('删除失败!');
}
// 修改成功后,需要触发列表刷新
ctx.fetchData();
} catch (e) {
setError(e);
} finally {
setLoading(false);
}
}, []);
const deleteHandler = () => {
// 删除学生
// http://localhost:1337/api/students/4
// props.stu.id
delStu();
};
const cancelEdit = () => {
setIsEdit(false);
};
return (
<>
{!isEdit &&
<tr>
<td>{props.stu.attributes.name}</td>
<td>{props.stu.attributes.gender}</td>
<td>{props.stu.attributes.age}</td>
<td>{props.stu.attributes.address}</td>
<td>
<button onClick={deleteHandler}>删除</button>
<button onClick={() => setIsEdit(true)}>修改</button>
</td>
</tr>
}
{isEdit && <StudentForm stu={props.stu} onCancel={cancelEdit}/>}
{loading && <tr>
<td colSpan={5}>正在删除数据...</td>
</tr>}
{error && <tr>
<td colSpan={5}>删除失败...</td>
</tr>}
</>
);
};
export default Student;
//StudentForm.js
import React, {useCallback, useContext, useState} from 'react';
import './StudentForm.css';
import StuContext from "../store/StuContext";
const StudentForm = (props) => {
const [inputData, setInputData] = useState({
name: props.stu ? props.stu.attributes.name : '',
age: props.stu ? props.stu.attributes.age : '',
gender: props.stu ? props.stu.attributes.gender : '男',
address: props.stu ? props.stu.attributes.address : ''
});
const ctx = useContext(StuContext);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
// 创建一个添加学生的方法
const addStudent = useCallback(async (newStu) => {
try {
setLoading(true);
setError(null);
//http://localhost:1337/api/students
const res = await fetch('http://localhost:1337/api/students', {
method: 'post',
body: JSON.stringify({data: newStu}),
headers: {
"Content-type": "application/json"
}
});
if (!res.ok) {
throw new Error('添加失败!');
}
ctx.fetchData();
} catch (e) {
console.log(e);
setError(e);
} finally {
setLoading(false);
}
}, []);
const updateStudent = useCallback(async (id, newStu)=>{
try {
setError(null);
setLoading(true);
const res = await fetch(`http://localhost:1337/api/students/${id}`,{
method:'put',
body:JSON.stringify({data:newStu}),
headers:{
"Content-type":'application/json'
}
});
if(!res.ok){
throw new Error('修改失败!');
}
ctx.fetchData();
}catch (e){
setError(e);
}finally {
setLoading(false);
}
}, []);
const nameChangeHandler = (e) => {
setInputData(prevState => ({...prevState, name: e.target.value}));
};
const ageChangeHandler = (e) => {
setInputData(prevState => ({...prevState, age: +e.target.value}));
};
const genderChangeHandler = (e) => {
setInputData(prevState => ({...prevState, gender: e.target.value}));
};
const addressChangeHandler = (e) => {
setInputData(prevState => ({...prevState, address: e.target.value}));
};
const submitHandler = () => {
addStudent(inputData);
};
const updateHandler = () => {
updateStudent(props.stu.id, inputData);
};
return (
<>
<tr className="student-form">
<td><input
onChange={nameChangeHandler}
value={inputData.name}
type="text"/></td>
<td>
<select
onChange={genderChangeHandler}
value={inputData.gender}
>
<option value="男">男</option>
<option value="女">女</option>
</select>
</td>
<td><input
onChange={ageChangeHandler}
value={inputData.age}
type="text"/></td>
<td><input
onChange={addressChangeHandler}
value={inputData.address}
type="text"/></td>
<td>
{props.stu && <>
<button onClick={()=>props.onCancel()}>取消</button>
<button onClick={updateHandler}>确认</button>
</>}
{!props.stu &&
<button
onClick={submitHandler}
>添加
</button>
}
</td>
</tr>
{loading && <tr>
<td colSpan={5}>添加中...</td>
</tr>}
{error && <tr>
<td colSpan={5}>添加失败</td>
</tr>}
</>
);
};
export default StudentForm;
九、自定义钩子
React中的钩子函数只能在函数组件或自定钩子中调用
理解:提取组件中重复的代码
-
当我们需要将React中钩子函数提取到一个公共区域时,就可以使用自定义钩子
-
自定义钩子其实就是一个普通函数,只是它的名字需要使用use开头
-
自定义钩子在组件一加载的时候就调用
要点:
-
cb 回调函数,请求发送成功后执行,如果在传参时传递了cb那么就调用该函数,如果没传就不管
- 将重新加载数据作为回调函数传递过来
-
reqObj 用来存储请求的参数
-
将body作为fetchData函数的参数进行传递的原因:如果把body放到reqObj中,自定义钩子在组件一加载时就调用,传递过来的body中的表单数据还没有值,将其作为fetchData的参数传递,等数据真正修改后点击提交再传递
//useFetch.js
import { useCallback, useState } from "react";
export default function useFetch(reqObj, cb) {
const [data, setData] = useState([]);
// 添加一个state来记录数据是否正在加载,false表示没有加载数据,true表示加载
const [loading, setLoading] = useState(false);
// 创建一个state来记录错误信息
const [error, setError] = useState(null);
const fetchData = useCallback(async (body) => {
try {
setLoading(true);
setError(null);
const res = await fetch('http://localhost:1337/api/' + reqObj.url, {
method: reqObj.method || 'get',
headers: {
"Content-type": "application/json"
},
body: body ? JSON.stringify({ data: body }) : null,
});
//判断请求是否加载成功
if (res.ok) {
const data = await res.json();
setData(data.data);
cb && cb();
} else {
throw new Error('数据加载失败!');
}
} catch (e) {
setError(e);
} finally {
setLoading(false);
}
}, []);
// 设置返回值
return {
loading,
error,
data,
fetchData
};
}
//Student.js
import React, {useCallback, useContext, useState} from 'react';
import StuContext from "../store/StuContext";
import StudentForm from "./StudentForm";
import useFetch from "../hooks/useFetch";
const Student = (props) => {
const [isEdit, setIsEdit] = useState(false);
const ctx = useContext(StuContext);
const {loading, error, fetchData:delStu} = useFetch({
url:`students/${props.stu.id}`,
method:'delete'
}, ctx.fetchData);
const deleteHandler = () => {
delStu();
};
const cancelEdit = () => {
setIsEdit(false);
};
return (
<>
{!isEdit &&
<tr>
<td>{props.stu.attributes.name}</td>
<td>{props.stu.attributes.gender}</td>
<td>{props.stu.attributes.age}</td>
<td>{props.stu.attributes.address}</td>
<td>
<button onClick={deleteHandler}>删除</button>
<button onClick={() => setIsEdit(true)}>修改</button>
</td>
</tr>
}
{isEdit && <StudentForm stu={props.stu} onCancel={cancelEdit}/>}
{loading && <tr>
<td colSpan={5}>正在删除数据...</td>
</tr>}
{error && <tr>
<td colSpan={5}>删除失败...</td>
</tr>}
</>
);
};
export default Student;
十、Redux
理解
-
state是在每个组件中声明和应用,如果state只是在当前组件中使用,自产自销是可以的
-
但是如果整个应用中使用state,我们以往的处理方式是将相关的state放入context中,其他组件通过context进行获取
- 缺点:需要先在它的组件中声明state然后传给context,每个state之间都是独立的
-
Redux是一个状态管理器,把state放到一起集中管理,相当于一个核心仓库,方便共享传递state
-
适用于大型应用
网页中使用redux的步骤
-
引入redux核心包
-
创建reducer整合函数
-
通过reducer对象创建store
-
对store中的state进行订阅
-
通过dispatch派发state的操作指令
<script src="https://unpkg.com/redux@4.2.0/dist/redux.js"></script>
-
state 表示当前state,可以根据这个state生成新的state
-
action 是一个js对象,它里边会保存操作的信息
- type表示操作的类型
- 其他需要传递的参数,也可以在action中设置
function reducer(state, action) {
switch (action.type) {
case 'ADD':
return state + 1;
case 'SUB':
return state - 1;
default:
return state;
}
}
const store = Redux.createStore(reducer, 1);
store.subscribe(() => {
countSpan.innerText = store.getState();
});
store.dispatch({type: 'SUB'});
state中的状态有多个时,完整代码如下:
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div>
<button id="sub">减少</button>
<span id="countSpan">1</span>
<span id="nameSpan"></span>
<button id="add">增加</button>
<button id="addFive">加5</button>
</div>
<script src="https://unpkg.com/redux@4.2.0/dist/redux.js"></script>
<script>
const subBtn = document.getElementById('sub');
const addBtn = document.getElementById('add');
const addFiveBtn = document.getElementById('addFive');
const countSpan = document.getElementById('countSpan');
const nameSpan = document.getElementById('nameSpan');
function reducer(state = {count:1,name:'孙悟空'}, action) {
switch (action.type) {
case 'ADD':
return {...state, count:state.count +1};
case 'SUB':
return {...state, count:state.count -1};
case 'ADD_N':
return {...state, count:state.count +action.payload};
default:
return state;
}
}
const store = Redux.createStore(reducer);
nameSpan.innerText = store.getState().name;
store.subscribe(() => {
countSpan.innerText = store.getState().count;
nameSpan.innerText = store.getState().name;
});
subBtn.addEventListener('click', () => {
store.dispatch({type: 'SUB'});
});
addBtn.addEventListener('click', () => {
store.dispatch({type: 'ADD'});
});
addFiveBtn.addEventListener('click', () => {
store.dispatch({type: 'ADD_N', payload:50});
});
</script>
</body>
</html>
上述代码存在的问题:
-
如果state过于复杂,将会非常难以维护
- 可以通过对state分组来解决这个问题,创建多个reducer,然后将其合并为一个
-
state每次操作时,都需要对state进行复制,然后再去修改
-
case后边的常量维护起来会比较麻烦
React中使用Redux
npm install -S redux react-redux
React中使用RTK(推荐)
1.RTK初使用
安装 npm install react-redux @reduxjs/toolkit -S
RTK:Redux工具包,帮助我们处理使用Redux过程中的重复性工作,简化Redux中的各种操作
使用RTK构建store,先在index.js中引入[将store注入到项目中去
]
//index.js
import ReactDOM from "react-dom/client";
import App from "./App";
import {Provider} from "react-redux";
import store from './store';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<App/>
</Provider>
);
createSlice 创建reducer的切片,它需要一个配置对象作为参数,通过对象的不同的属性来指定它的配置
- name:'XXX', reducer的名字,会作为action中type属性的前缀,不要重复
- initialState:{State} state的初始值
- reducers:{setState(state,action{})} reducer的具体方法,需要一个对象作为参数,可以以方法的形式添加reducer,RTK会自动生成action对象
createSlice返回的并不是一个reducer对象而是一个slice对象(切片对象)。这个对象中我们需要使用的属性现在有两个一个叫做actions,一个叫做reducer
💕总结:
- 使用createSlice创建切片后,切片会自动根据配置对象生成action和reducer
- action需要导出给调用处,调用处可以使用action作为dispatch的参数触发state的修改
- reducer需要传递给configureStore以使其在仓库中生效
//store.js
//使用RTK来构建store
import {configureStore, createSlice} from "@reduxjs/toolkit";
const stuSlice = createSlice({ //[创建切片对象]
name:'stu', // 用来自动生成action中的type
initialState:{
name:'孙悟空',
age:18,
gender:'男',
address:'花果山'
}, // state的初始值
reducers:{ // 指定state的各种操作,直接在对象中添对stat操作的方法
setName(state, action){
// 可以通过不同的方法来指定对state的不同操作
// 两个参数:state 这个state的是一个代理对象,可以直接修改
state.name = action.payload;
},
setAge(state, action){
state.age = action.payload;
}
}
});
// 切片对象会自动的帮助我们生成action
// actions中存储的是slice自动生成action创建器(函数),调用函数后会自动创建action对象
// action对象的结构 {type:name/函数名, payload:函数的参数}
export const {setName, setAge} = stuSlice.actions;
// const nameAction = setName('哈哈');
// const ageAction = setAge(30);
// 创建store 用来创建store对象,需要一个配置对象作为参数 [创建store对象]
const store = configureStore({
reducer:{
student:stuSlice.reducer
}
});
export default store;
//App.js
import React from 'react';
import {useDispatch, useSelector} from "react-redux";
import {setName, setAge} from './store';
const App = () => {
// useSelector() 用来加载state中的数据 [获取state]
const student = useSelector(state => state.student);
// 通过useDispatch()来获取派发器对象 [获取派发器]
const dispatch = useDispatch();
// 获取action的构建器
const setNameHandler = () => {
dispatch(setName('沙和尚'));
};
const setAgeHandler = () => {
dispatch(setAge(33));
};
return (
<div>
<p>
{student.name} ---
{student.age} ---
{student.gender} ---
{student.address}
</p>
<button onClick={setNameHandler}>修改name</button>
<button onClick={setAgeHandler}>修改age</button>
</div>
);
};
export default App;
2.拆分RTK
//index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App'
import { Provider } from 'react-redux';
import store from './store/store.js'
document.documentElement.style.fontSize = 100 / 750 + 'vw';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<App />
</Provider>
);
//App.js
import React from 'react'
import { useDispatch, useSelector } from "react-redux";
import { setName, setAge } from './store/stuSlice'
import { setName as setSchoolName, setAddress } from './store/schoolSlice'
function App() {
const { student, school } = useSelector(state => state)
const dispatch = useDispatch()
const setNameHandler = () => {
dispatch(setName("麦味"))
}
const setAgeHandler = () => {
dispatch(setAge(24))
}
return (
<div>
<p>
{student.name} ---
{student.age} ---
{student.gender} ---
{student.address}
</p>
<button onClick={setNameHandler}>修改name</button>
<button onClick={setAgeHandler}>修改age</button>
<hr />
<p>
{school.name} ---
{school.address}
</p>
<button onClick={() => dispatch(setSchoolName("内蒙古大学"))}>修改name</button>
<button onClick={() => dispatch(setAddress("内蒙古赛罕区"))}>修改Address</button>
</div>
)
}
export default App
//stuSlice.js
import { createSlice } from "@reduxjs/toolkit";
const stuSlice=createSlice({
name:'stu',
initialState:{
name:'米粒',
age:23,
gender:'女',
address:'内蒙古'
},
reducers:{
setName(state,action){
state.name=action.payload;
},
setAge(state,action){
state.age=action.payload;
}
}
})
export const {setName,setAge } = stuSlice.actions
export const {reducer:stuReducer}=stuSlice
//schoolSlice.js
import { createSlice } from "@reduxjs/toolkit";
const schoolSlice = createSlice({
name: 'school',
initialState: {
name: '西安交通大学',
address: '陕西省西安市长安区'
},
reducers: {
setName(state, action) {
state.name = action.payload
},
setAddress(state, action) {
state.address = action.payload
}
}
})
export const { setName, setAddress } = schoolSlice.actions
export const { reducer: schoolReducer } = schoolSlice
//store.js
import { configureStore } from "@reduxjs/toolkit";
import { stuReducer } from './stuSlice'
import {schoolReducer} from './schoolSlice'
const store = configureStore({
reducer: {
student: stuReducer,
school:schoolReducer
}
});
export default store;
RTKQ
用来处理数据加载的问题,RTK Query是一个强大的数据获取和缓存工具
1.创建Api切片
createApi() 用来创建RTKQ中的API对象
RTKQ的所有功能都需要通过该对象来进行
createApi() 需要一个对象作为参数
-
endpoints是一个回调函数,回调函数的返回值是一个对象。
-
对象中属性名就是要实现的功能名
- 比如获取所有学生可以命名为getStudents,根据id获取学生可以命名为getStudentById。
-
属性值要通过build对象创建,分两种情况:
- 查询:
build.query({})
- 增删改:
build.mutation({})
两个query:
-
一个是build的query方法
-
一个是query方法配置对象中的属性,这个方法需要返回一个子路径,这个子路径将会和baseUrl拼接为一个完整的请求路径
getStudets的最终请求地址是:
http://localhost:1337/api/
+students
=http://localhost:1337/api/students
- 查询:
-
build.query({})
查询方法
//studentApi.js
import {createApi, fetchBaseQuery} from "@reduxjs/toolkit/dist/query/react";
const studentApi = createApi({
reducerPath: 'studentApi', // Api的标识,不能和其他的Api或reducer重复
baseQuery: fetchBaseQuery({
baseUrl: "http://localhost:1337/api/"
}),// 指定查询的基础信息,发送请求使用的工具
endpoints(build) {
// build是请求的构建器,通过build来设置请求的相关信息
return {
getStudents:build.query({
query() {
// 用来指定请求子路径
return 'students';
}
}),
};
}// endpoints 用来指定Api中的各种功能,是一个方法,需要一个对象作为返回值
});
export const {
useGetStudentsQuery
} = studentApi;
export default studentApi;
-
Api对象创建后,对象中会根据各种方法自动的生成对应的钩子函数
-
通过这些钩子函数,可以来向服务器发送请求
-
钩子函数的命名规则 getStudents --> useGetStudentsQuery
-
获取并将钩子向外部暴露 export const {useGetStudentsQuery} = studentApi;
build.mutation({})方式
删除方法
...
delStudent: build.mutation({
query(id) {
return {
// 如果发送的不是get请求,需要返回一个对象来设置请求的信息
url: `students/${id}`,
method: 'delete'
};
}
})
...
获取删除的钩子,useMutation的钩子返回的是一个数组
数组中有两个东西,第一个是操作的触发器,第二个是结果集
import { useDelStudentMutation } from '../store/studentApi'
...
const [delStudent, { isSuccess }] = useDelStudentMutation()
const deleteHandler = () => {
delStudent(props.stu.id);
};
...
增加方法
...
addStudent: build.mutation({
query(stu) {
return {
url: 'students',
method: 'post',
body: { data: stu }
}
},
invalidatesTags: [{ type: 'student', id: 'LIST' }]
}),
...
import { useAddStudentMutation } from '../store/studentApi'
...
const [addStudent, { isSuccess: isAddSuccess }] = useAddStudentMutation()
const addHandler = () => {
addStudent(inputData)
setInputData({
name: '',
gender: '男',
age: '',
address: ''
})
}
...
修改方法
...
updateStudent: build.mutation({
query(stu) {
return {
url: `students/${stu.id}`,
method: 'put',
body: { data: stu.attributes }
}
},
// 列表和当前修改项失效,其他数据项不失效,使当前数据正常修改后再刷新整个列表,保证当前值是最新值
invalidatesTags: ((result, error, stu) =>
[{ type: 'student', id: stu.id }, { type: 'student', id: 'LIST' }])
})
...
const updateHandler = () => {
updateStudent({
id: props.stuId,
attributes: inputData
})
props.onCancel();
}
2.创建Store对象
// store/index.js
import {configureStore} from "@reduxjs/toolkit";
import studentApi from "./studentApi";
const store = configureStore({
reducer:{
[studentApi.reducerPath]:studentApi.reducer
},
middleware:getDefaultMiddleware =>
getDefaultMiddleware().concat(studentApi.middleware)
});
export default store;
[studentApi.reducerPath]表示用变量作为属性名使用
中间件:对store进行扩展,添加一个中间件,使缓存生效
store创建完毕同样要在index.js中设置Provider标签
//index.js
import {Provider} from "react-redux";
import store from './store';
<Provider store={store}>
<App/>
</Provider>
//App.js
import React from 'react';
import {useGetStudentsQuery} from "./store/studentApi";
const App = () => {
// 调用Api查询数据
// 这个钩子函数它会返回一个对象作为返回值,请求过程中相关数据都在该对象中存储
const {data, isSuccess, isLoading} = useGetStudentsQuery(); // 调用Api中的钩子查询数据
return (
<div>
{isLoading && <p>数据加载中...</p>}
{isSuccess && data.data.map(item => <p key={item.id}>
{item.attributes.name} ---
{item.attributes.age} ---
{item.attributes.gender} ---
{item.attributes.address}
</p>)}
</div>
);
};
export default App;
直接调用useGetStudentsQuery()会自动向服务器发送请求加载数据,并返回一个对象。这个对象中包括了很多属性:
- data – 最新返回的数据
- currentData – 当前的数据(可以不用currentData只用data)
- error – 错误信息
- isUninitialized – 如果为true则表示查询还没开始
- isLoading – 为true时,表示请求正在第一次加载
- isFetching 为true时,表示请求正在加载
- isSuccess 为true时,表示请求发送成功
- isError 为true时,表示请求有错误
- refetch 函数,用来重新加载数据
useQuery可以接收一个对象作为第二个参数,通过该对象可以对请求进行配置
const result = useGetStudentsQuery(null, {
// useQuery可以接收一个对象作为第二个参数,通过该对象可以对请求进行配置
selectFromResult: result => {
if (result.data) {
result.data = result.data.filter(item => item.attributes.age < 18);
}
return result;
}, // 用来指定useQuery返回的结果
});
const result = useGetStudentsQuery(null, {
// useQuery可以接收一个对象作为第二个参数,通过该对象可以对请求进行配置
pollingInterval:0, // 设置轮询的间隔,轮询发送请求,单位毫秒 如果为0则表示不轮询(场景:聊天室)
skip:false, // 设置是否跳过当前请求,默认false
refetchOnMountOrArgChange:false, // 设置是否每次都重新加载数据 false正常使用缓存,
// true每次都重载数据数字,数据缓存的时间(秒)
refetchOnFocus:false, // 是否在重新获取焦点时重载数据
refetchOnReconnect:true, // 是否在重新连接后重载数据(断网)
});
const { data: stuData, isLoading, isSuccess } = useGetStudentByIdQuery(props.stuId, {
skip: !props.stuId
})
skip:当没有传入参数时,就跳过该请求
refetchOnMountOrArgChange:每次组件重新渲染,是否重新发请求
- false当对同一数据发送请求时,使用缓存;
- true每次都重新加载
import { configureStore } from "@reduxjs/toolkit";
import { setupListeners } from "@reduxjs/toolkit/dist/query";
import studentApi from "./studentApi";
const store = configureStore({
reducer:{
[studentApi.reducerPath]:studentApi.reducer
},
middleware: getDefaultMiddleware =>
getDefaultMiddleware().concat(studentApi.middleware)
})
setupListeners(store.dispatch)//设置以后会支持refetchOnFocus、refetchOnReconnect
export default store
append #数据标签
由于数据存在缓存,数据不会自动刷新,只会显示之前缓存中的数据,而增加和修改后的数据必须通过刷新整个列表才能在列表中显示出最新的数据
在RTKQ中可以给每个查询的api打印一个标签,当标签失效时,就可以自动触发失效标签
完整代码(注释版本)
//在对应api的js文件中
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/dist/query/react";
const studentApi = createApi({
reducePath: 'studentApi',
baseQuery: fetchBaseQuery({
baseUrl: 'http://localhost:1337/api'
}),
tagTypes: ['student'], // 用来指定Api中的标签类型
endpoints(build) {
return {
getStudents: build.query({
query() {
return 'students'
},
// transformResponse用来转换响应数据的格式
transformResponse(basequeryReturnValue) {
// 返回整个数据的data属性
return basequeryReturnValue.data
},
providesTags: [{ type: 'student', id: 'LIST' }]
}),
getStudentById: build.query({
query(id) {
return `students/${id}`
},
transformResponse(basequeryReturnValue) {
// 返回整个数据的data属性
return basequeryReturnValue.data
},
// providesTags: (result, error, id) => [{ type: 'student', id }]
providesTags: ['student']
}),
delStudent: build.mutation({
query(id) {
return {
// 如果发送的不是get请求,需要返回一个对象来设置请求的信息
url: `students/${id}`,
method: 'delete'
};
}
}),
addStudent: build.mutation({
query(stu) {
return {
url: 'students',
method: 'post',
body: { data: stu }
}
},
invalidatesTags: [{ type: 'student', id: 'LIST' }]
}),
updateStudent: build.mutation({
query(stu) {
return {
url: `students/${stu.id}`,
method: 'put',
body: { data: stu.attributes }
}
},
// 列表和当前修改项失效,其他数据项不失效,使当前数据正常修改后再刷新整个列表,保证当前值是最新值
invalidatesTags: ((result, error, stu) =>
[{ type: 'student', id: stu.id }, { type: 'student', id: 'LIST' }])
})
}
}
})
export const {
useGetStudentsQuery,
useGetStudentByIdQuery,
useDelStudentMutation,
useAddStudentMutation,
useUpdateStudentMutation
} = studentApi
export default studentApi
十一、路由
基本使用
目前学习router6的主要内容
安装 npm install react-router-dom@6 -S
或 yarn add react-router-dom@6
1.将路由注入项目
//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.querySelector('#root'));
root.render(
<Router>
<App />
</Router>
);
2.对路由与url进行映射
Route组件必须放到Routes中,当浏览器的地址发生变化时,会自动对Routes中的所有Route进行匹配,匹配到的则显示,其余Route则不再继续匹配
< Routes>
-
是Routes v6 中新增加的组件,作用和Switch类似,都是用于Route的容器
-
Routes中Route只有一个会被匹配
< Route>
-
v6中,Route的component render children都变了
-
需要通过element来指定要挂载的组件
//App.js
import React from 'react'
import { Routes, Route } from 'react-router-dom'
import Home from './components/Home'
import About from './components/About'
import Menu from './components/Menu'
const App=()=> {
return (
<div>
<h1>hello,router</h1>
<Menu></Menu>
<Routes>
<Route path="/" element={<Home></Home>}></Route>
{/* 可以不写斜杠 */}
<Route path="/about" element={<About/>}></Route>
<Route path='student/:id' element={<Student></Student>}></Route>
</Routes>
</div>
)
}
export default App
//Menu.js
import React from 'react'
import { Link } from 'react-router-dom'
const Menu=()=> {
return (
<ul>
<li>
<Link to="/">主页</Link>
</li>
<li>
<Link to="/about">关于</Link>
</li>
</ul>
)
}
export default Menu
参数和钩子
//Student.js
import React from 'react'
import { useLocation, useParams, useMatch, useNavigate } from "react-router-dom"
const STU_DATA = [
{
id: 1,
name: '刘备'
},
{
id: 2,
name: '关羽'
},
{
id: 3,
name: '沙和尚'
},
{
id: 4,
name: '唐僧'
},
];
const Student = () => {
const { id } = useParams()//获取参数
const stu = STU_DATA.find((item) => { return item.id === +id })
const location = useLocation()
console.log('location', location);
// 判断当前路由与路径是否匹配 路径匹配返回对象,不匹配返回null
const match = useMatch("/student/:id")
console.log('match', match);
// 用于跳转页面的函数
const navigate = useNavigate()
const clickHandler = () => {
// 使用replace不会产生新的记录,替代
navigate('/about', { replace: true })
}
return (
<div>
<button onClick={clickHandler} > 点我一下</button >
<h2>{stu.id}--------{stu.name}</h2>
</div >
)
}
export default Student
嵌套路由和Outlet
旧有的嵌套路由的写法不方便维护,将路由与子路由统一写到一个地方,方便管理
解决方案:在标签体中嵌套路由
访问路径/Home/StuInfo
//index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter, Route, Routes, Navigate } from "react-router-dom";
import './index.css';
import Home from './views/Home/Home';
import Login from './views/Login/Login';
import Register from './views/Register/Register'
import StuInfo from './components/StuInfo/StuInfo';
import AddClocking from './components/AddClocking/AddClocking';
import Unsampled from './components/Unsampled/Unsampled';
import Sampling from './components/Sampling/Sampling';
import ClockIn from './components/ClockIn/ClockIn';
import Account from './components/Account/Account';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<BrowserRouter>
<Routes>
<Route path="/" element={<Login />}></Route>
<Route path="/Register" element={<Register />}></Route>
<Route path="/Home" element={<Home />}>
<Route path='/Home' element={<Navigate to='StuInfo' />} />
<Route path='StuInfo' element={<StuInfo />}></Route>
<Route path='AddClocking' element={<AddClocking />}></Route>
<Route path='Unsampled' element={<Unsampled />}></Route>
<Route path='Sampling' element={<Sampling />}></Route>
<Route path='ClockIn' element={<ClockIn />}></Route>
<Route path='Account' element={<Account />}></Route>
</Route>
</Routes>
</BrowserRouter>
);
在Home组件下,写占位符,表示组件显示在哪个位置,使用< Outlet/>
Outlet 用来表示嵌套路由中的组件
- 当嵌套路由中的路径匹配成功了,Outlet则表示嵌套路由中的组件
- 当嵌套路由中的路径没有匹配成功,Outlet就什么都不是
import {Outlet} from "react-router-dom";
//<Outlet/>放到组件需要显示的位置
Navigate组件
用来跳转页面,作用与钩子useNavigate类似
Navigate 组件用来跳转到指定的位置,默认使用push跳转
<Navigate to="/student/1" replace/>
NavLink组件
//Menu.js
import React from 'react';
import {Link, NavLink} from "react-router-dom";
const Menu = () => {
return (
<div>
<ul>
<li>
<Link to="/">主页</Link>
</li>
<li>
<Link to="/about">关于</Link>
</li>
<li>
<NavLink
style={
({isActive})=>{
return isActive?
{backgroundColor:"yellow"}:
null
}
}
to="/student/2">学生</NavLink>
</li>
</ul>
</div>
);
};
export default Menu;
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步