前言
最近刚学完 React,想着把笔记分享给大家,本笔记特别适合从事后端想要学习前端的人。我看视频是黑马最新的 React 视频(黑马程序员前端React18入门到实战视频教程,从react+hooks核心基础到企业级项目开发实战(B站评论、极客园项目等)及大厂面试全通关_哔哩哔哩_bilibili),个人觉得讲得还不错的。想要完整版可以私信我,如果对你有帮助的话就点个赞关注下吧。后面持续分享 Java 相关技术和笔记。
一、React 基础
1. 创建一个 react 项目
1.利用 create-react-app 工具创建一个 react 项目
npx create-react-app project-name
npm start # 启动项目
2.src 目录只保留 App.js 和 index.js 文件
3.精简 App.js 和 index.js 文件
1.1 src 目录下文件的作用
index.js 是项目的入口,从这里开始运行,App 是根组件被 Index.js 导入,最后渲染到 index.html 中 root 节点上
index.js:
// 项目的核心入口 从这里开始运行
// React 必要的两个核心包
import React from 'react';
import ReactDOM from 'react-dom/client';
// 导入项目的根组件
import App from './App';
// 把 App 根组件渲染到 id 为 root 的 dom 节点上
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<App />
);
App:
// 项目的根组件
// App -> index.js -> public/index.html(root) => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上
function App() {
return (
<div className="App">
this is React App
</div>
);
}
export default App;
2. jsx 基础-概念和本质
2.1 JSX 是什么?
JSX 表示在 JS 代码中编写 HTML 模板结构,是 React 中编写 UI 模板的方式
优势:
-
HTML 的声明式模板写法
-
JS 的可编程能力
JSX 是 JS 的拓展,浏览器不可直接识别,需要解析工具解析才可识别
2.2 JSX 编写 JS 代码
在 jsx 中可通过大括号 {} 识别 js 表达式,比如常见的变量、函数调用、方法调用等等
App.js:
// 项目的根组件
// App -> index.js -> public/index.html(root) => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上
function getName() {
return 'jack';
}
const count = 100;
function App() {
return (
<div className="App">
this is React App
{/*1. 引号传递字符串*/}
{'this is message'}
{/*2. 识别 js 变量*/}
{count}
{/*3. 函数调用*/}
{ getName() }
{/*4. 方法调用*/}
{new Date().getDate()}
{/*5. 使用 js 对象*/}
<div style={{ color: 'red' }}>this is div</div>
</div>
);
}
export default App;
2.3 JSX 中实现列表渲染
提示:在 JSX 中可以使用原生 JS 中 map 方法遍历渲染列表
// 项目的根组件
// App -> index.js -> public/index.html(root) => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上
const count = 100;
const list = [
{id: 1001, name: 'Vue'},
{id: 1002, name: 'React'},
{id: 1003, name: 'Angular'},
];
function App() {
return (
<div className="App">
this is App
{/*渲染列表*/}
{list.map(item => <li key={item.id}>{item.name}</li>)}
</div>
);
}
export default App;
注意:
-
渲染哪个结构就 return 那个
-
循环渲染记得要加上独一无二的 key(类型为 string 或 number)
2.4 JSX 实现条件渲染
在 React 中,可以通过逻辑与运算符 &&、三元表达式(?:) 实现**基础的条件渲染*
类似 Vue 的 v-if
// 项目的根组件
// App -> index.js -> public/index.html(root) => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上
const isLogin = true;
function App() {
return (
<div className="App">
{/*1. 逻辑与 &&*/}
{isLogin && <span>this is span</span>}
<br/>
{/*2. 三元运算*/}
{isLogin ? <span>is Login</span> : <span>not Login</span>}
</div>
);
}
export default App;
2.4.1 JSX 条件渲染的 demo
// 项目的根组件
// App -> index.js -> public/index.html(root) => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上
const articleType = 3; // 0 1 3 'articleType' 的取值范围
// 定义核心函数(根据文章类型返回不同的 JSX 模板)
function getArticleTemplate() {
if (articleType === 0) {
return <div>我是图文文章</div>
} else if (articleType === 1) {
return <div>我是单图文章</div>
} else {
return <div>我是三图文章</div>
}
}
function App() {
return (
<div className="App">
{/*调用函数渲染不同模板*/}
{getArticleTemplate()}
</div>
);
}
export default App;
显示:我是三图文章
3. React 基础事件绑定
语法:on + 事件名 = {事件处理程序/函数名},遵循驼峰命令
1.绑定事件
// 项目的根组件
// App -> index.js -> public/index.html(root) => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上
function App() {
const handleClick = () => {
console.log('button 被点击了');
}
return (
<div className="App">
<button onClick={handleClick}>click me</button>
</div>
);
}
export default App;
2.传递事件参数 e
// 项目的根组件
// App -> index.js -> public/index.html(root) => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上
function App() {
// 拿到事件参数 e
const handleClick = (e) => {
console.log('button 被点击了', e);
}
return (
<div className="App">
<button onClick={handleClick}>click me</button>
</div>
);
}
export default App;
3.传递自定义参数
// 项目的根组件
// App -> index.js -> public/index.html(root) => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上
function App() {
// 传递自定义参数
const handleClick = (name) => {
console.log('button 被点击了', name);
}
return (
<div className="App">
{/*箭头函数传参*/}
<button onClick={() => handleClick('jack')}>click me</button>
</div>
);
}
export default App;
4.同时传递自定义参数和事件参数 e
// 项目的根组件
// App -> index.js -> public/index.html(root) => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上
function App() {
// const handleClick = () => {
// console.log('button 被点击了');
// }
// 拿到事件参数 e
// const handleClick = (e) => {
// console.log('button 被点击了', e);
// }
const handleClick = (name, e) => {
console.log('button 被点击了', name, e);
}
return (
<div className="App">
{/*箭头函数传参*/}
<button onClick={(e) => handleClick('jack', e)}>click me</button>
</div>
);
}
export default App;
4. React组件
在 React 中,一个组件就是一个首字母大写的函数 ,内部含有组件的逻辑和 UI,渲染组件只需将组件当做标签书写即可
1.定义组件(function 定义或者箭头函数)
App.js:
function Button() {
// 组件逻辑
return <button>click me</button>
}
const Button = () => {
// 组件逻辑
return <button>click me</button>
}
2.渲染组件(自闭和或成对标签)
function App() {
return (
<div className="App">
{/*自闭和*/}
<Button />
{/*成对标签*/}
<Button></Button>
</div>
);
}
5. useState 基础使用
其是 React 的一个 Hook,允许我们向组件添加一个状态变量,从而控制影响组件的渲染结果
const [count, setCount] = useState(0);
count 的值不可直接修改,只能通过 setCount 修改
本质:状态变量一旦发生变化组件的视图 UI 也会变化(数据驱动视图)
特点:
-
useState 是一个函数,返回值是一个数组
-
数组的第一个参数是状态变量,第二个参数是 set 函数用来修改状态变量
-
useState 的参数将作为 count 的初始值
一个 useState 的小 demo
App.js:
// 项目的根组件
// App -> index.js -> public/index.html(root) => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上
// useState 实现一个计数器按钮
import {useState} from "react";
function App() {
// 1. 调用 useState 添加一个状态变量
// count 状态变量
// setCount 修改状态变量
const [count, setCount] = useState(0);
// 2. 点击按钮的回调
const handleClick = () => {
// 作用:1.用传入的新值修改 count
// 2.重新使用新的 count 渲染 UI
setCount(count + 1);
};
return (
<div className="App">
<button onClick={handleClick}>{count}</button>
</div>
);
}
export default App;
拓展 demo
App.js:
// 项目的根组件
// App -> index.js -> public/index.html(root) => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上
// useState 实现一个计数器按钮
import {useState} from "react";
function App() {
// 修改对象
const [form, setForm] = useState({name: 'jack'});
const changeForm = () => {
// 错误写法:直接修改
// form.name = 'john';
// 正确写法:setForm 传入一个全新的对象
setForm({
...form,
name: 'john',
})
};
return (
<div className="App">
<button onClick={changeForm}>修改 form {form.name}</button>
</div>
);
}
export default App;
6. 如何修改组件样式
App.js:
// 项目的根组件
// App -> index.js -> public/index.html(root) => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上
// 导入样式
import './index.css'
const style = {
color: 'red',
fontSize: '50px',
};
function App() {
return (
<div className="App">
{/*行内控制*/}
<span style={{color: 'red', fontSize: '50px'}}>this is span</span>
<span style={style}>this is span</span>
{/*通过 class 类名控制*/}
<span className='foo'>this is foo</span>
</div>
);
}
export default App;
index.css:
.foo {
color: blue;
}
7. B 站评论案例
7.1 列表渲染
App.js:
import './App.scss'
import avatar from './images/bozai.png'
import {useState} from "react";
/**
* 评论列表的渲染和操作
*
* 1. 根据状态渲染评论列表
* 2. 删除评论
*/
// 评论列表数据
const list = [
{
// 评论id
rpid: 3,
// 用户信息
user: {
uid: '13258165',
avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
uname: '周杰伦',
},
// 评论内容
content: '哎哟,不错哦',
// 评论时间
ctime: '10-18 08:15',
like: 88,
},
{
rpid: 2,
user: {
uid: '36080105',
avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
uname: '许嵩',
},
content: '我寻你千百度 日出到迟暮',
ctime: '11-13 11:29',
like: 88,
},
{
rpid: 1,
user: {
uid: '30009257',
avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
uname: '黑马前端',
},
content: '学前端就来黑马',
ctime: '10-19 09:00',
like: 66,
},
]
// 当前登录用户信息
const user = {
// 用户id
uid: '30009257',
// 用户头像
avatar,
// 用户昵称
uname: '黑马前端',
}
/**
* 导航 Tab 的渲染和操作
*
* 1. 渲染导航 Tab 和高亮
* 2. 评论列表排序
* 最热 => 喜欢数量降序
* 最新 => 创建时间降序
*/
// 导航 Tab 数组
const tabs = [
{ type: 'hot', text: '最热' },
{ type: 'time', text: '最新' },
]
// 渲染评论列表
// 1.使用 useState 维护 list
const App = () => {
const [commentList, setCommentList] = useState(list);
return (
<div className="app">
{/* 导航 Tab */}
<div className="reply-navigation">
<ul className="nav-bar">
<li className="nav-title">
<span className="nav-title-text">评论</span>
{/* 评论数量 */}
<span className="total-reply">{10}</span>
</li>
<li className="nav-sort">
{/* 高亮类名: active */}
<span className='nav-item'>最新</span>
<span className='nav-item'>最热</span>
</li>
</ul>
</div>
<div className="reply-wrap">
{/* 发表评论 */}
<div className="box-normal">
{/* 当前用户头像 */}
<div className="reply-box-avatar">
<div className="bili-avatar">
<img className="bili-avatar-img" src={avatar} alt="用户头像" />
</div>
</div>
<div className="reply-box-wrap">
{/* 评论框 */}
<textarea
className="reply-box-textarea"
placeholder="发一条友善的评论"
/>
{/* 发布按钮 */}
<div className="reply-box-send">
<div className="send-text">发布</div>
</div>
</div>
</div>
{/* 评论列表 */}
<div className="reply-list">
{/* 评论项 */}
{commentList.map(item => (
<div key={item.rpid} className="reply-item">
{/* 头像 */}
<div className="root-reply-avatar">
<div className="bili-avatar">
<img
className="bili-avatar-img"
alt=""
src={item.user.avatar}
/>
</div>
</div>
<div className="content-wrap">
{/* 用户名 */}
<div className="user-info">
<div className="user-name">{item.user.uname}</div>
</div>
{/* 评论内容 */}
<div className="root-reply">
<span className="reply-content">{item.content}</span>
<div className="reply-info">
{/* 评论时间 */}
<span className="reply-time">{item.ctime}</span>
{/* 评论数量 */}
<span className="reply-time">点赞数:{item.like}</span>
<span className="delete-btn">
删除
</span>
</div>
</div>
</div>
</div>
))}
</div>
</div>
</div>
)
}
export default App
App.scss:
.app {
width: 80%;
margin: 50px auto;
}
.reply-navigation {
margin-bottom: 22px;
.nav-bar {
display: flex;
align-items: center;
margin: 0;
padding: 0;
list-style: none;
.nav-title {
display: flex;
align-items: center;
width: 114px;
font-size: 20px;
.nav-title-text {
color: #18191c;
font-weight: 500;
}
.total-reply {
margin: 0 36px 0 6px;
color: #9499a0;
font-weight: normal;
font-size: 13px;
}
}
.nav-sort {
display: flex;
align-items: center;
color: #9499a0;
font-size: 13px;
.nav-item {
cursor: pointer;
&:hover {
color: #00aeec;
}
&:last-child::after {
display: none;
}
&::after {
content: ' ';
display: inline-block;
height: 10px;
width: 1px;
margin: -1px 12px;
background-color: #9499a0;
}
}
.nav-item.active {
color: #18191c;
}
}
}
}
.reply-wrap {
position: relative;
}
.box-normal {
display: flex;
transition: 0.2s;
.reply-box-avatar {
display: flex;
align-items: center;
justify-content: center;
width: 80px;
height: 50px;
}
.reply-box-wrap {
display: flex;
position: relative;
flex: 1;
.reply-box-textarea {
width: 100%;
height: 50px;
padding: 5px 10px;
box-sizing: border-box;
color: #181931;
font-family: inherit;
line-height: 38px;
background-color: #f1f2f3;
border: 1px solid #f1f2f3;
border-radius: 6px;
outline: none;
resize: none;
transition: 0.2s;
&::placeholder {
color: #9499a0;
font-size: 12px;
}
&:focus {
height: 60px;
background-color: #fff;
border-color: #c9ccd0;
}
}
}
.reply-box-send {
position: relative;
display: flex;
flex-basis: 86px;
align-items: center;
justify-content: center;
margin-left: 10px;
border-radius: 4px;
cursor: pointer;
transition: 0.2s;
& .send-text {
position: absolute;
z-index: 1;
color: #fff;
font-size: 16px;
}
&::after {
position: absolute;
width: 100%;
height: 100%;
background-color: #00aeec;
border-radius: 4px;
opacity: 0.5;
content: '';
}
&:hover::after {
opacity: 1;
}
}
}
.bili-avatar {
position: relative;
display: block;
width: 48px;
height: 48px;
margin: 0;
padding: 0;
border-radius: 50%;
}
.bili-avatar-img {
position: absolute;
top: 50%;
left: 50%;
display: block;
width: 48px;
height: 48px;
object-fit: cover;
border: none;
border-radius: 50%;
image-rendering: -webkit-optimize-contrast;
transform: translate(-50%, -50%);
}
// 评论列表
.reply-list {
margin-top: 14px;
}
.reply-item {
padding: 22px 0 0 80px;
.root-reply-avatar {
position: absolute;
left: 0;
display: flex;
justify-content: center;
width: 80px;
cursor: pointer;
}
.content-wrap {
position: relative;
flex: 1;
&::after {
content: ' ';
display: block;
height: 1px;
width: 100%;
margin-top: 14px;
background-color: #e3e5e7;
}
.user-info {
display: flex;
align-items: center;
margin-bottom: 4px;
.user-name {
height: 30px;
margin-right: 5px;
color: #61666d;
font-size: 13px;
line-height: 30px;
cursor: pointer;
}
}
.root-reply {
position: relative;
padding: 2px 0;
color: #181931;
font-size: 15px;
line-height: 24px;
.reply-info {
position: relative;
display: flex;
align-items: center;
margin-top: 2px;
color: #9499a0;
font-size: 13px;
.reply-time {
width: 86px;
margin-right: 20px;
}
.reply-like {
display: flex;
align-items: center;
margin-right: 19px;
.like-icon {
width: 14px;
height: 14px;
margin-right: 5px;
color: #9499a0;
background-position: -153px -25px;
&:hover {
background-position: -218px -25px;
}
}
.like-icon.liked {
background-position: -154px -89px;
}
}
.reply-dislike {
display: flex;
align-items: center;
margin-right: 19px;
.dislike-icon {
width: 16px;
height: 16px;
background-position: -153px -153px;
&:hover {
background-position: -217px -153px;
}
}
.dislike-icon.disliked {
background-position: -154px -217px;
}
}
.delete-btn {
cursor: pointer;
&:hover {
color: #00aeec;
}
}
}
}
}
}
.reply-none {
height: 64px;
margin-bottom: 80px;
color: #99a2aa;
font-size: 13px;
line-height: 64px;
text-align: center;
}
7.2 删除功能实现
需求:
-
只有自己的评论才可以删除
-
点击删除按钮,删除当前评论,列表中不再显示
核心思路:
-
删除显示 - 条件渲染
-
删除功能 - 拿到当前项 id 以 id 为条件对评论列表做过滤
7.3 渲染 Tab + 点击高亮实现
需求:点击哪个 tab 项,哪个做高亮处理
核心思路:
点击谁就把谁的type(独一无二的标识)记录下来,然后和遍历时的每一项的type做匹配,谁匹配到就设置负责高亮的类名
import './App.scss'
import avatar from './images/bozai.png'
import {useState} from "react";
/**
* 评论列表的渲染和操作
*
* 1. 根据状态渲染评论列表
* 2. 删除评论
*/
// 评论列表数据
const list = [
{
// 评论id
rpid: 3,
// 用户信息
user: {
uid: '13258165',
avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
uname: '周杰伦',
},
// 评论内容
content: '哎哟,不错哦',
// 评论时间
ctime: '10-18 08:15',
like: 88,
},
{
rpid: 2,
user: {
uid: '36080105',
avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
uname: '许嵩',
},
content: '我寻你千百度 日出到迟暮',
ctime: '11-13 11:29',
like: 88,
},
{
rpid: 1,
user: {
uid: '30009257',
avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
uname: '黑马前端',
},
content: '学前端就来黑马',
ctime: '10-19 09:00',
like: 66,
},
]
// 当前登录用户信息
const user = {
// 用户id
uid: '30009257',
// 用户头像
avatar,
// 用户昵称
uname: '黑马前端',
}
/**
* 导航 Tab 的渲染和操作
*
* 1. 渲染导航 Tab 和高亮
* 2. 评论列表排序
* 最热 => 喜欢数量降序
* 最新 => 创建时间降序
*/
// 导航 Tab 数组
const tabs = [
{ type: 'hot', text: '最热' },
{ type: 'time', text: '最新' },
]
const App = () => {
// 渲染评论列表
// 1.使用 useState 维护 list
const [commentList, setCommentList] = useState(list);
// 删除功能
const handleDel = (id) => {
// 对 commentList 进行过滤
setCommentList(commentList.filter(item => item.rpid !== id));
};
// tab 切换功能
// 1.点击谁就把谁的 type 记录下来
// 2.通过记录 type 和每一项遍历时的 type 做匹配 控制激活类名的显示
const [type, setType] = useState('hot');
const handleTabChange = (type) => {
console.log(type);
setType(type);
};
return (
<div className="app">
{/* 导航 Tab */}
<div className="reply-navigation">
<ul className="nav-bar">
<li className="nav-title">
<span className="nav-title-text">评论</span>
{/* 评论数量 */}
<span className="total-reply">{10}</span>
</li>
<li className="nav-sort">
{/* 高亮类名: active */}
{tabs.map(item =>
<span key={item.type}
onClick={() => handleTabChange(item.type)}
className={`nav-item ${type === item.type && 'active'}`}>
{item.text}
</span>)}
</li>
</ul>
</div>
<div className="reply-wrap">
{/* 发表评论 */}
<div className="box-normal">
{/* 当前用户头像 */}
<div className="reply-box-avatar">
<div className="bili-avatar">
<img className="bili-avatar-img" src={avatar} alt="用户头像" />
</div>
</div>
<div className="reply-box-wrap">
{/* 评论框 */}
<textarea
className="reply-box-textarea"
placeholder="发一条友善的评论"
/>
{/* 发布按钮 */}
<div className="reply-box-send">
<div className="send-text">发布</div>
</div>
</div>
</div>
{/* 评论列表 */}
<div className="reply-list">
{/* 评论项 */}
{commentList.map(item => (
<div key={item.rpid} className="reply-item">
{/* 头像 */}
<div className="root-reply-avatar">
<div className="bili-avatar">
<img
className="bili-avatar-img"
alt=""
src={item.user.avatar}
/>
</div>
</div>
<div className="content-wrap">
{/* 用户名 */}
<div className="user-info">
<div className="user-name">{item.user.uname}</div>
</div>
{/* 评论内容 */}
<div className="root-reply">
<span className="reply-content">{item.content}</span>
<div className="reply-info">
{/* 评论时间 */}
<span className="reply-time">{item.ctime}</span>
{/* 评论数量 */}
<span className="reply-time">点赞数:{item.like}</span>
{/*条件:user.id === item.user.id*/}
{user.uid === item.user.uid && <span className="delete-btn" onClick={() => handleDel(item.rpid)}>删除</span>}
</div>
</div>
</div>
</div>
))}
</div>
</div>
</div>
)
}
export default App
7.4 排序功能
需求:点击最新,评论列表按照创建时间倒序排列(新的在前),点击最热按照点赞数排序(多的在前)
核心思路:把评论列表状态数据进行不同的排序处理,当成新值传给 set 函数重新渲染视图 UI
lodash 库
安装:
npm install lodash
引入:
import _ from 'lodash';
使用:
setCommentList(_.orderBy(commentList, 'like', 'desc'));
App.js:
import './App.scss'
import avatar from './images/bozai.png'
import {useState} from "react";
import _ from 'lodash';
/**
* 评论列表的渲染和操作
*
* 1. 根据状态渲染评论列表
* 2. 删除评论
*/
// 评论列表数据
const list = [
{
// 评论id
rpid: 3,
// 用户信息
user: {
uid: '13258165',
avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
uname: '周杰伦',
},
// 评论内容
content: '哎哟,不错哦',
// 评论时间
ctime: '10-20 08:15',
like: 38,
},
{
rpid: 2,
user: {
uid: '36080105',
avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
uname: '许嵩',
},
content: '我寻你千百度 日出到迟暮',
ctime: '09-13 11:29',
like: 88,
},
{
rpid: 1,
user: {
uid: '30009257',
avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
uname: '黑马前端',
},
content: '学前端就来黑马',
ctime: '10-19 09:00',
like: 66,
},
]
// 当前登录用户信息
const user = {
// 用户id
uid: '30009257',
// 用户头像
avatar,
// 用户昵称
uname: '黑马前端',
}
/**
* 导航 Tab 的渲染和操作
*
* 1. 渲染导航 Tab 和高亮
* 2. 评论列表排序
* 最热 => 喜欢数量降序
* 最新 => 创建时间降序
*/
// 导航 Tab 数组
const tabs = [
{ type: 'hot', text: '最热' },
{ type: 'time', text: '最新' },
]
const App = () => {
// 渲染评论列表
// 1.使用 useState 维护 list
const [commentList, setCommentList] = useState(_.orderBy(list, 'like', 'desc'));
// 删除功能
const handleDel = (id) => {
// 对 commentList 进行过滤
setCommentList(commentList.filter(item => item.rpid !== id));
};
// tab 切换功能
// 1.点击谁就把谁的 type 记录下来
// 2.通过记录 type 和每一项遍历时的 type 做匹配 控制激活类名的显示
const [type, setType] = useState('hot');
const handleTabChange = (type) => {
console.log(type);
setType(type);
// 基于列表的排序
if (type === 'hot') {
// 根据点赞数量排序
// lodash
setCommentList(_.orderBy(commentList, 'like', 'desc'));
} else {
// 根据创建时间排序
setCommentList(_.orderBy(commentList, 'ctime', 'desc'))
}
};
return (
<div className="app">
{/* 导航 Tab */}
<div className="reply-navigation">
<ul className="nav-bar">
<li className="nav-title">
<span className="nav-title-text">评论</span>
{/* 评论数量 */}
<span className="total-reply">{10}</span>
</li>
<li className="nav-sort">
{/* 高亮类名: active */}
{tabs.map(item =>
<span key={item.type}
onClick={() => handleTabChange(item.type)}
className={`nav-item ${type === item.type && 'active'}`}>
{item.text}
</span>)}
</li>
</ul>
</div>
<div className="reply-wrap">
{/* 发表评论 */}
<div className="box-normal">
{/* 当前用户头像 */}
<div className="reply-box-avatar">
<div className="bili-avatar">
<img className="bili-avatar-img" src={avatar} alt="用户头像" />
</div>
</div>
<div className="reply-box-wrap">
{/* 评论框 */}
<textarea
className="reply-box-textarea"
placeholder="发一条友善的评论"
/>
{/* 发布按钮 */}
<div className="reply-box-send">
<div className="send-text">发布</div>
</div>
</div>
</div>
{/* 评论列表 */}
<div className="reply-list">
{/* 评论项 */}
{commentList.map(item => (
<div key={item.rpid} className="reply-item">
{/* 头像 */}
<div className="root-reply-avatar">
<div className="bili-avatar">
<img
className="bili-avatar-img"
alt=""
src={item.user.avatar}
/>
</div>
</div>
<div className="content-wrap">
{/* 用户名 */}
<div className="user-info">
<div className="user-name">{item.user.uname}</div>
</div>
{/* 评论内容 */}
<div className="root-reply">
<span className="reply-content">{item.content}</span>
<div className="reply-info">
{/* 评论时间 */}
<span className="reply-time">{item.ctime}</span>
{/* 评论数量 */}
<span className="reply-time">点赞数:{item.like}</span>
{/*条件:user.id === item.user.id*/}
{user.uid === item.user.uid && <span className="delete-btn" onClick={() => handleDel(item.rpid)}>删除</span>}
</div>
</div>
</div>
</div>
))}
</div>
</div>
</div>
)
}
export default App
8. classnams 优化类名控制
classnams 是一个简单的 JS 库,可以非常方便的通过条件动态控制 class 类名的显示
以前出现的问题:
语法:key 表示要控制的类名,value 表示条件,true 的时候类名就会显示
使用
安装:
npm install classnames
引入:
import classNames from "classnames";
用法:
<span key={item.type}
onClick={() => handleTabChange(item.type)}
className={classNames('nav-item', {active: type === item.type})}>
{item.text}
</span>)}
9. 受表单控制项
概念:使用 React 组件的状态(useState)控制表单状态
App.js
// 项目的根组件
// App -> index.js -> public/index.html(root) => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上
// 1.声明一个 react 状态 - useState
// 2.核心绑定流程
// 2.1通过 value 属性绑定 react 状态
// 2.2绑定 onChange 事件,通过事件参数 e 拿到输入框最新值,反向修改 react 状态
import {useState} from "react";
function App() {
const [value, setValue] = useState();
return (
<div className="App">
<input
value={value}
onChange={(e) => setValue(e.target.value)}
type='text'
/>
</div>
);
}
export default App;
10. React 中获取DOM
在 React 中获取/操作 DOM,需要使用 useRef 钩子函数,分为两步:
-
使用 useRef 创建 ref 对象,并与 JSX 绑定
-
在 DOM 可用/DOM 渲染完毕时,通过 inputRef.current 拿到 DOM 对象
App.js:
import React, { useRef } from "react";
function App() {
const inputRef = useRef(null);
const showDom = () => {
console.log(inputRef.current);
};
const setInputValue = () => {
inputRef.current.value = '新的值';
};
const focusInput = () => {
inputRef.current.focus();
};
const selectInputText = () => {
inputRef.current.select();
};
const addClassToInput = () => {
inputRef.current.classList.add('new-class');
};
const removeClassFromInput = () => {
inputRef.current.classList.remove('new-class');
};
const addEventListenerToInput = () => {
inputRef.current.addEventListener('input', (event) => {
console.log('Input changed:', event.target.value);
});
};
return (
<div className="App">
<input ref={inputRef} type='text'/>
<button onClick={showDom}>获取 dom</button>
<button onClick={setInputValue}>设置值</button>
<button onClick={focusInput}>聚焦</button>
<button onClick={selectInputText}>选择文本</button>
<button onClick={addClassToInput}>添加类</button>
<button onClick={removeClassFromInput}>移除类</button>
<button onClick={addEventListenerToInput}>添加事件监听器</button>
</div>
);
}
export default App;
11.B 站评论优化
11.1 发表评论
App.scss:
.app {
width: 80%;
margin: 50px auto;
}
.reply-navigation {
margin-bottom: 22px;
.nav-bar {
display: flex;
align-items: center;
margin: 0;
padding: 0;
list-style: none;
.nav-title {
display: flex;
align-items: center;
width: 114px;
font-size: 20px;
.nav-title-text {
color: #18191c;
font-weight: 500;
}
.total-reply {
margin: 0 36px 0 6px;
color: #9499a0;
font-weight: normal;
font-size: 13px;
}
}
.nav-sort {
display: flex;
align-items: center;
color: #9499a0;
font-size: 13px;
.nav-item {
cursor: pointer;
&:hover {
color: #00aeec;
}
&:last-child::after {
display: none;
}
&::after {
content: ' ';
display: inline-block;
height: 10px;
width: 1px;
margin: -1px 12px;
background-color: #9499a0;
}
}
.nav-item.active {
color: #18191c;
}
}
}
}
.reply-wrap {
position: relative;
}
.box-normal {
display: flex;
transition: 0.2s;
.reply-box-avatar {
display: flex;
align-items: center;
justify-content: center;
width: 80px;
height: 50px;
}
.reply-box-wrap {
display: flex;
position: relative;
flex: 1;
.reply-box-textarea {
width: 100%;
height: 50px;
padding: 5px 10px;
box-sizing: border-box;
color: #181931;
font-family: inherit;
line-height: 38px;
background-color: #f1f2f3;
border: 1px solid #f1f2f3;
border-radius: 6px;
outline: none;
resize: none;
transition: 0.2s;
&::placeholder {
color: #9499a0;
font-size: 12px;
}
&:focus {
height: 60px;
background-color: #fff;
border-color: #c9ccd0;
}
}
}
.reply-box-send {
position: relative;
display: flex;
flex-basis: 86px;
align-items: center;
justify-content: center;
margin-left: 10px;
border-radius: 4px;
cursor: pointer;
transition: 0.2s;
& .send-text {
position: absolute;
z-index: 1;
color: #fff;
font-size: 16px;
}
&::after {
position: absolute;
width: 100%;
height: 100%;
background-color: #00aeec;
border-radius: 4px;
opacity: 0.5;
content: '';
}
&:hover::after {
opacity: 1;
}
}
}
.bili-avatar {
position: relative;
display: block;
width: 48px;
height: 48px;
margin: 0;
padding: 0;
border-radius: 50%;
}
.bili-avatar-img {
position: absolute;
top: 50%;
left: 50%;
display: block;
width: 48px;
height: 48px;
object-fit: cover;
border: none;
border-radius: 50%;
image-rendering: -webkit-optimize-contrast;
transform: translate(-50%, -50%);
}
// 评论列表
.reply-list {
margin-top: 14px;
}
.reply-item {
padding: 22px 0 0 80px;
.root-reply-avatar {
position: absolute;
left: 0;
display: flex;
justify-content: center;
width: 80px;
cursor: pointer;
}
.content-wrap {
position: relative;
flex: 1;
&::after {
content: ' ';
display: block;
height: 1px;
width: 100%;
margin-top: 14px;
background-color: #e3e5e7;
}
.user-info {
display: flex;
align-items: center;
margin-bottom: 4px;
.user-name {
height: 30px;
margin-right: 5px;
color: #61666d;
font-size: 13px;
line-height: 30px;
cursor: pointer;
}
}
.root-reply {
position: relative;
padding: 2px 0;
color: #181931;
font-size: 15px;
line-height: 24px;
.reply-info {
position: relative;
display: flex;
align-items: center;
margin-top: 2px;
color: #9499a0;
font-size: 13px;
.reply-time {
width: 86px;
margin-right: 20px;
}
.reply-like {
display: flex;
align-items: center;
margin-right: 19px;
.like-icon {
width: 14px;
height: 14px;
margin-right: 5px;
color: #9499a0;
background-position: -153px -25px;
&:hover {
background-position: -218px -25px;
}
}
.like-icon.liked {
background-position: -154px -89px;
}
}
.reply-dislike {
display: flex;
align-items: center;
margin-right: 19px;
.dislike-icon {
width: 16px;
height: 16px;
background-position: -153px -153px;
&:hover {
background-position: -217px -153px;
}
}
.dislike-icon.disliked {
background-position: -154px -217px;
}
}
.delete-btn {
cursor: pointer;
&:hover {
color: #00aeec;
}
}
}
}
}
}
.reply-none {
height: 64px;
margin-bottom: 80px;
color: #99a2aa;
font-size: 13px;
line-height: 64px;
text-align: center;
}
App.js:
import './App.scss'
import avatar from './images/bozai.png'
import {useState} from "react";
import _ from 'lodash';
import classNames from "classnames";
/**
* 评论列表的渲染和操作
*
* 1. 根据状态渲染评论列表
* 2. 删除评论
*/
// 评论列表数据
const list = [
{
// 评论id
rpid: 3,
// 用户信息
user: {
uid: '13258165',
avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
uname: '周杰伦',
},
// 评论内容
content: '哎哟,不错哦',
// 评论时间
ctime: '10-20 08:15',
like: 38,
},
{
rpid: 2,
user: {
uid: '36080105',
avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
uname: '许嵩',
},
content: '我寻你千百度 日出到迟暮',
ctime: '09-13 11:29',
like: 88,
},
{
rpid: 1,
user: {
uid: '30009257',
avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
uname: '黑马前端',
},
content: '学前端就来黑马',
ctime: '10-19 09:00',
like: 66,
},
]
// 当前登录用户信息
const user = {
// 用户id
uid: '30009257',
// 用户头像
avatar,
// 用户昵称
uname: '黑马前端',
}
/**
* 导航 Tab 的渲染和操作
*
* 1. 渲染导航 Tab 和高亮
* 2. 评论列表排序
* 最热 => 喜欢数量降序
* 最新 => 创建时间降序
*/
// 导航 Tab 数组
const tabs = [
{ type: 'hot', text: '最热' },
{ type: 'time', text: '最新' },
]
const App = () => {
// 渲染评论列表
// 1.使用 useState 维护 list
const [commentList, setCommentList] = useState(_.orderBy(list, 'like', 'desc'));
// 删除功能
const handleDel = (id) => {
// 对 commentList 进行过滤
setCommentList(commentList.filter(item => item.rpid !== id));
};
// tab 切换功能
// 1.点击谁就把谁的 type 记录下来
// 2.通过记录 type 和每一项遍历时的 type 做匹配 控制激活类名的显示
const [type, setType] = useState('hot');
const handleTabChange = (type) => {
console.log(type);
setType(type);
// 基于列表的排序
if (type === 'hot') {
// 根据点赞数量排序
// lodash
setCommentList(_.orderBy(commentList, 'like', 'desc'));
} else {
// 根据创建时间排序
setCommentList(_.orderBy(commentList, 'ctime', 'desc'))
}
};
// 发表评论
const [content, setContent] = useState('');
const handlePublish = () => {
setCommentList([
...commentList,
{
rpid: 4,
user: {
uid: '30009257',
avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
uname: '黑马前端',
},
content: content,
ctime: '10-19 09:00',
like: 80,
}
]);
};
return (
<div className="app">
{/* 导航 Tab */}
<div className="reply-navigation">
<ul className="nav-bar">
<li className="nav-title">
<span className="nav-title-text">评论</span>
{/* 评论数量 */}
<span className="total-reply">{10}</span>
</li>
<li className="nav-sort">
{/* 高亮类名: active */}
{tabs.map(item =>
<span key={item.type}
onClick={() => handleTabChange(item.type)}
className={classNames('nav-item', {active: type === item.type})}>
{item.text}
</span>)
}
</li>
</ul>
</div>
<div className="reply-wrap">
{/* 发表评论 */}
<div className="box-normal">
{/* 当前用户头像 */}
<div className="reply-box-avatar">
<div className="bili-avatar">
<img className="bili-avatar-img" src={avatar} alt="用户头像" />
</div>
</div>
<div className="reply-box-wrap">
{/* 评论框 */}
<textarea
className="reply-box-textarea"
placeholder="发一条友善的评论"
value={content}
onChange={(e) => setContent(e.target.value)}
/>
{/* 发布按钮 */}
<div className="reply-box-send">
<div className="send-text" onClick={handlePublish}>发布</div>
</div>
</div>
</div>
{/* 评论列表 */}
<div className="reply-list">
{/* 评论项 */}
{commentList.map(item => (
<div key={item.rpid} className="reply-item">
{/* 头像 */}
<div className="root-reply-avatar">
<div className="bili-avatar">
<img
className="bili-avatar-img"
alt=""
src={item.user.avatar}
/>
</div>
</div>
<div className="content-wrap">
{/* 用户名 */}
<div className="user-info">
<div className="user-name">{item.user.uname}</div>
</div>
{/* 评论内容 */}
<div className="root-reply">
<span className="reply-content">{item.content}</span>
<div className="reply-info">
{/* 评论时间 */}
<span className="reply-time">{item.ctime}</span>
{/* 评论数量 */}
<span className="reply-time">点赞数:{item.like}</span>
{/*条件:user.id === item.user.id*/}
{user.uid === item.user.uid && <span className="delete-btn" onClick={() => handleDel(item.rpid)}>删除</span>}
</div>
</div>
</div>
</div>
))}
</div>
</div>
</div>
)
}
export default App
uuid 库
安装:
npm install uuid
引入:
import {v4 as uuidV4} from 'uuid'
uuidV4(); // 使用
dayjs 库
安装:
npm install dayjs
引入:
import dayjs from 'dayjs'
dayjs() // 使用
11.2 发表评论后清除输入框并聚焦
思路:
-
设置输入框的 useState 的 setContent 为空
-
利用 useRef 获取 dom 元素,再调用 focus 方法
App.js:
import './App.scss'
import avatar from './images/bozai.png'
import {useRef, useState} from "react";
import _ from 'lodash';
import classNames from "classnames";
import {v4 as uuidV4} from 'uuid'
import dayjs from "dayjs";
/**
* 评论列表的渲染和操作
*
* 1. 根据状态渲染评论列表
* 2. 删除评论
*/
// 评论列表数据
const list = [
{
// 评论id
rpid: 3,
// 用户信息
user: {
uid: '13258165',
avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
uname: '周杰伦',
},
// 评论内容
content: '哎哟,不错哦',
// 评论时间
ctime: '10-20 08:15',
like: 38,
},
{
rpid: 2,
user: {
uid: '36080105',
avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
uname: '许嵩',
},
content: '我寻你千百度 日出到迟暮',
ctime: '09-13 11:29',
like: 88,
},
{
rpid: 1,
user: {
uid: '30009257',
avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
uname: '黑马前端',
},
content: '学前端就来黑马',
ctime: '10-19 09:00',
like: 66,
},
]
// 当前登录用户信息
const user = {
// 用户id
uid: '30009257',
// 用户头像
avatar,
// 用户昵称
uname: '黑马前端',
}
/**
* 导航 Tab 的渲染和操作
*
* 1. 渲染导航 Tab 和高亮
* 2. 评论列表排序
* 最热 => 喜欢数量降序
* 最新 => 创建时间降序
*/
// 导航 Tab 数组
const tabs = [
{ type: 'hot', text: '最热' },
{ type: 'time', text: '最新' },
]
const App = () => {
// 渲染评论列表
// 1.使用 useState 维护 list
const [commentList, setCommentList] = useState(_.orderBy(list, 'like', 'desc'));
const inputRef = useRef(null);
// 删除功能
const handleDel = (id) => {
// 对 commentList 进行过滤
setCommentList(commentList.filter(item => item.rpid !== id));
};
// tab 切换功能
// 1.点击谁就把谁的 type 记录下来
// 2.通过记录 type 和每一项遍历时的 type 做匹配 控制激活类名的显示
const [type, setType] = useState('hot');
const handleTabChange = (type) => {
console.log(type);
setType(type);
// 基于列表的排序
if (type === 'hot') {
// 根据点赞数量排序
// lodash
setCommentList(_.orderBy(commentList, 'like', 'desc'));
} else {
// 根据创建时间排序
setCommentList(_.orderBy(commentList, 'ctime', 'desc'))
}
};
// 发表评论
const [content, setContent] = useState('');
const handlePublish = () => {
setCommentList([
...commentList,
{
rpid: uuidV4(),
user: {
uid: '30009257',
avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
uname: '黑马前端',
},
content: content,
ctime: dayjs(new Date()).format('MM-DD hh:mm'),
like: 80,
}
]);
// 1.清楚输入框内容
setContent('')
// 2.重新聚焦
inputRef.current.focus();
};
return (
<div className="app">
{/* 导航 Tab */}
<div className="reply-navigation">
<ul className="nav-bar">
<li className="nav-title">
<span className="nav-title-text">评论</span>
{/* 评论数量 */}
<span className="total-reply">{10}</span>
</li>
<li className="nav-sort">
{/* 高亮类名: active */}
{tabs.map(item =>
<span key={item.type}
onClick={() => handleTabChange(item.type)}
className={classNames('nav-item', {active: type === item.type})}>
{item.text}
</span>)
}
</li>
</ul>
</div>
<div className="reply-wrap">
{/* 发表评论 */}
<div className="box-normal">
{/* 当前用户头像 */}
<div className="reply-box-avatar">
<div className="bili-avatar">
<img className="bili-avatar-img" src={avatar} alt="用户头像" />
</div>
</div>
<div className="reply-box-wrap">
{/* 评论框 */}
<textarea
className="reply-box-textarea"
placeholder="发一条友善的评论"
value={content}
onChange={(e) => setContent(e.target.value)}
ref={inputRef}
/>
{/* 发布按钮 */}
<div className="reply-box-send">
<div className="send-text" onClick={handlePublish}>发布</div>
</div>
</div>
</div>
{/* 评论列表 */}
<div className="reply-list">
{/* 评论项 */}
{commentList.map(item => (
<div key={item.rpid} className="reply-item">
{/* 头像 */}
<div className="root-reply-avatar">
<div className="bili-avatar">
<img
className="bili-avatar-img"
alt=""
src={item.user.avatar}
/>
</div>
</div>
<div className="content-wrap">
{/* 用户名 */}
<div className="user-info">
<div className="user-name">{item.user.uname}</div>
</div>
{/* 评论内容 */}
<div className="root-reply">
<span className="reply-content">{item.content}</span>
<div className="reply-info">
{/* 评论时间 */}
<span className="reply-time">{item.ctime}</span>
{/* 评论数量 */}
<span className="reply-time">点赞数:{item.like}</span>
{/*条件:user.id === item.user.id*/}
{user.uid === item.user.uid && <span className="delete-btn" onClick={() => handleDel(item.rpid)}>删除</span>}
</div>
</div>
</div>
</div>
))}
</div>
</div>
</div>
)
}
export default App
12. 组件间通信
父传子 props
传递步骤:
1.父组件传递数据,子组件标签身上绑定属性 2.子组件接收数据,props 参数
父传子 demo:
App.js:
import React from "react";
// 父传子
// 1.父组件传递数据,子组件标签身上绑定属性
// 2.子组件接收数据,props 参数
function Son (props) {
// props:对象包含了父组件传递过来的所有数据
console.log(props);
return <div>this is son, father's param is {props.name}</div>;
}
function App() {
const name = 'this is app name';
return (
<div className="App">
<Son name={name} />
</div>
);
}
export default App;
小 demo:
App.js:
import React from "react";
// 父传子
// 1.父组件传递数据,子组件标签身上绑定属性
// 2.子组件接收数据,props 参数
function Son (props) {
// props:对象包含了父组件传递过来的所有数据
console.log(props);
return <div>this is son, father's param is {props.name}</div>;
}
function App() {
const name = 'this is app name';
return (
<div className="App">
<Son
name={name}
age={18}
isTrue={false}
list={['vue', 'react']}
cb={() => console.log(123)}
child={<span>this is span</span>}
/>
</div>
);
}
export default App;
注意:
父组件几乎可以给子组件传任何东西,包括布尔,数值,数组,对象和函数等
但是子组件不可修改父组件传递的属性,谁传递的谁修改
父传子 特殊的 prop children
组件包裹传递
App.js:
import React from "react";
// 父传子
// 1.父组件传递数据,子组件标签身上绑定属性
// 2.子组件接收数据,props 参数
function Son (props) {
console.log(props)
return <div>this is son, {props.children}</div>
}
function App() {
return (
<div className="App">
<Son>
<span>this is span</span>
</Son>
</div>
);
}
export default App;
显示:this is son,this is span
子传父
思路:子组件调用父组件中的函数并传递参数
App.js:
import React, {useState} from "react";
// 核心:在子组件中调用父组件中的函数并传递实参
function Son ({onGetSonMsg}) {
const sonMsg = 'this is son msg';
return (
<div>
this is son
<button onClick={() => onGetSonMsg(sonMsg)}>sendMsg</button>
</div>
);
}
function App() {
const [msg, setMsg] = useState('');
const getMsg = (msg) => {
console.log(msg);
setMsg(msg);
};
return (
<div className="App">
this is App, {msg}
<Son onGetSonMsg={getMsg} />
</div>
);
}
export default App;
使用状态提升实现兄弟组件通信
思路:借助"状态提升"机制,通过父组件进行兄弟组件之间的数据传递
App.js:
import React, {useState} from "react";
// 1.子传父 A -> App
// 2.子传父 B -> App
function A ({onGetAName}) {
// A 组件中的数据
const name = 'this is A name';
return (
<div>
this is A component
<button onClick={() => onGetAName(name)}>send</button>
</div>
);
}
function B (props) {
return (
<div>
this is B component, {props.name}
</div>
);
}
function App() {
const [name, setName] = useState('');
const getAName = (name) => {
console.log(name);
setName(name);
};
return (
<div className="App">
this is App
<A onGetAName={getAName}/>
<B name={name}/>
</div>
);
}
export default App;
使用 context 机制跨层级组件通信
实现步骤:
-
使用createContext方法创建一个上下文对象Ctx
-
在顶层组件(App)中通过Ctx.Provider组件提供数据
-
在底层组件(B)中通过useContext钩子函数获取消费数据
App.js:
import React, {createContext, useContext} from "react";
// App -> A -> B
// 1.createContext 方法创建一个上下文对象
const MsgContext = createContext();
// 2.在顶层组件通过 Provider 组件提供数据
// 3.在底层组件通过 useContext 钩子函数使用数据
function A () {
return (
<div>
this is A component
<B />
</div>
);
}
function B () {
const msg = useContext(MsgContext);
return (
<div>
this is B component, {msg}
</div>
);
}
function App() {
const msg = 'this is app msg';
return (
<div className="App">
<MsgContext.Provider value={msg}>
this is App
<A />
</MsgContext.Provider>
</div>
);
}
export default App;
结果:
使用场景:
13. UseEffect概念理解
useEffect 用于在 React 组件中创建不是由事件引起而是由渲染本身引起的操作,比如发送 ajax 请求,更改 DOM 等。
语法:
useEffect(() => {}, [])
参数1是一个函数,可以把它叫做副作用函数,在内部放置要执行的操作
参数2是一个数组,在数组里放置依赖项,不同依赖项会影响第一个参数函数的执行。当是空数组时,副作用函数只会在组件渲染完毕后执行一次。
App.js:
import React, {useEffect, useState} from "react";
const URL = 'http://geek.itheima.net/v1_0/channels';
function App() {
// 创建一个状态数据
const [list, setList] = useState([]);
useEffect(() => {
// 额外的操作 获取频道列表
async function getList() {
const res = await fetch(URL);
const jsonRes = await res.json();
console.log(jsonRes);
setList(jsonRes.data.channels);
}
getList();
}, []);
return (
<div className="App">
<ul>
{list.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
</div>
);
}
export default App;
useEffect 依赖参数说明
情况一:
function App() {
// 1. 没有依赖项 初始 + 组件更新
const [count, setCount] = useState(0);
useEffect(() => {
console.log('副作用函数执行了');
});
return (
<div className="App">
this is app
<button onClick={() => {setCount(count + 1)}}>+{count}</button>
</div>
);
}
export default App;
情况二:
function App() {
// 1. 没有依赖项 初始 + 组件更新
const [count, setCount] = useState(0);
// useEffect(() => {
// console.log('副作用函数执行了');
// });
// 2. 传入空数组依赖 只在初始渲染时执行
useEffect(() => {
console.log('副作用函数执行了');
}, []);
return (
<div className="App">
this is app
<button onClick={() => {setCount(count + 1)}}>+{count}</button>
</div>
);
}
export default App;
情况三:
import React, {useEffect, useState} from "react";
function App() {
// 1. 没有依赖项 初始 + 组件更新
const [count, setCount] = useState(0);
// useEffect(() => {
// console.log('副作用函数执行了');
// });
// 2. 传入空数组依赖 只在初始渲染时执行
// useEffect(() => {
// console.log('副作用函数执行了');
// }, []);
// 3. 传入特定依赖项 初始 + 依赖项变化时执行
useEffect(() => {
console.log('副作用函数执行了');
}, [count]);
return (
<div className="App">
this is app
<button onClick={() => {setCount(count + 1)}}>+{count}</button>
</div>
);
}
export default App;
useEffect 清除副作用
在useEffect中编写的由渲染本身引起的对接组件外部的操作,社区也经常把它叫做副作用操作,比如在useEffect中开 启了一个定时器,我们想在组件卸载时把这个定时器再清理掉,这个过程就是清理副作用
import React, {useEffect, useState} from "react";
function Son() {
useEffect(() => {
const timer = setInterval(() => {
console.log('定时器执行中……')
}, 1000);
return () => {
// 清除副作用(组件卸载时)
clearInterval(timer);
};
}, []);
return <div>this is son</div>
}
function App() {
const [show, setShow] = useState(true);
return (
<div className="App">
{show && <Son />}
<button onClick={() => setShow(false)}>卸载 Son 组件</button>
</div>
);
}
export default App;
**说明:**清除副作用的函数最常见的执行时机是组件卸载时自动执行
**需求:**Son 组件渲染时开启一个定时器,卸载时清除它
14. 自定义 Hook 函数
概念:自定义 Hook 是以 use 开头的函数,通过自定义 Hook 函数可以用来实现逻辑的封装和复用
封装思路:
-
声明一个以 use 开头的函数
-
在函数体内封装可复用的逻辑(只要是可复用的逻辑)
-
返回状态和回调(以对象或者数据返回)
-
在哪个组件中要用到,就执行这个函数,解构出状态和回调即可使用
import React, {useEffect, useState} from "react";
// 问题:布尔切换逻辑与当前组件耦合在一起,不方便使用
// 解决思路:自定义 hook
function useToggle() {
// 可复用的代码逻辑
const [value, setValue] = useState(true);
const toggle = () => setValue(!value);
// 哪些状态和回调函数需要在其他组件使用 return
return {
value,
toggle
}
}
function App() {
const { value, toggle } = useToggle();
return (
<div className="App">
{value && <div>this is div</div>}
<button onClick={toggle}>toggle</button>
</div>
);
}
export default App;
15. ReactHooks 使用规则
-
只能在组件中或者其他自定义Hook函数中调用
-
只能在组件的顶层调用,不能嵌套在 if、for、其他函数中
import React, {useEffect, useState} from "react";
// 问题:布尔切换逻辑与当前组件耦合在一起,不方便使用
// 解决思路:自定义 hook
function useToggle() {
// 可复用的代码逻辑
const [value, setValue] = useState(true);
const toggle = () => setValue(!value);
// 哪些状态和回调函数需要在其他组件使用 return
return {
value,
toggle
}
}
function App() {
const { value, toggle } = useToggle();
return (
<div className="App">
{value && <div>this is div</div>}
<button onClick={toggle}>toggle</button>
</div>
);
}
export default App;
16. B 站评论优化
模拟请求评论接口,抽象出 Hook
实现思路:
1.使用 json-server 工具模拟接口服务,通过 axios 发送接口请求
2.使用 useEffect 调用接口获取数据
安装 json-server 库:
npm i json-server -D
安装 axios 库:
npm install axios
db.json:
{
"list": [
{
"rpid": 3,
"user": {
"uid": "13258165",
"avatar": "http://toutiao.itheima.net/resources/images/98.jpg",
"uname": "周杰伦"
},
"content": "哎哟,不错哦",
"ctime": "10-18 08: 15",
"like": 126
},
{
"rpid": 2,
"user": {
"uid": "36080105",
"avatar": "http://toutiao.itheima.net/resources/images/98.jpg",
"uname": "许嵩"
},
"content": "我寻你千百度 日出到迟暮",
"ctime": "11-13 11: 29",
"like": 88
},
{
"rpid": 1,
"user": {
"uid": "30009257",
"avatar": "http://toutiao.itheima.net/resources/images/98.jpg",
"uname": "黑马前端"
},
"content": "学前端就来黑马",
"ctime": "10-19 09: 00",
"like": 66
}
]
}
改写 package.json:
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"serve": "json-server db.json --port 3004"
},
App.js:
import './App.scss'
import avatar from './images/bozai.png'
import {useEffect, useRef, useState} from "react";
import _ from 'lodash';
import classNames from "classnames";
import {v4 as uuidV4} from 'uuid'
import dayjs from "dayjs";
import axios from "axios";
/**
* 评论列表的渲染和操作
*
* 1. 根据状态渲染评论列表
* 2. 删除评论
*/
// 评论列表数据
const list = [
{
// 评论id
rpid: 3,
// 用户信息
user: {
uid: '13258165',
avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
uname: '周杰伦',
},
// 评论内容
content: '哎哟,不错哦',
// 评论时间
ctime: '10-20 08:15',
like: 38,
},
{
rpid: 2,
user: {
uid: '36080105',
avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
uname: '许嵩',
},
content: '我寻你千百度 日出到迟暮',
ctime: '09-13 11:29',
like: 88,
},
{
rpid: 1,
user: {
uid: '30009257',
avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
uname: '黑马前端',
},
content: '学前端就来黑马',
ctime: '10-19 09:00',
like: 66,
},
]
// 当前登录用户信息
const user = {
// 用户id
uid: '30009257',
// 用户头像
avatar,
// 用户昵称
uname: '黑马前端',
}
/**
* 导航 Tab 的渲染和操作
*
* 1. 渲染导航 Tab 和高亮
* 2. 评论列表排序
* 最热 => 喜欢数量降序
* 最新 => 创建时间降序
*/
// 导航 Tab 数组
const tabs = [
{ type: 'hot', text: '最热' },
{ type: 'time', text: '最新' },
]
const App = () => {
// 渲染评论列表
// 1.使用 useState 维护 list
// const [commentList, setCommentList] = useState(_.orderBy(list, 'like', 'desc'));
// 获取接口数据渲染
const [commentList, setCommentList] = useState([]);
useEffect( () => {
// 请求数据
async function getList() {
// axios 请求数据
const res = await axios.get('http://localhost:3004/list');
setCommentList(res.data);
}
getList();
}, []);
const inputRef = useRef(null);
// 删除功能
const handleDel = (id) => {
// 对 commentList 进行过滤
setCommentList(commentList.filter(item => item.rpid !== id));
};
// tab 切换功能
// 1.点击谁就把谁的 type 记录下来
// 2.通过记录 type 和每一项遍历时的 type 做匹配 控制激活类名的显示
const [type, setType] = useState('hot');
const handleTabChange = (type) => {
console.log(type);
setType(type);
// 基于列表的排序
if (type === 'hot') {
// 根据点赞数量排序
// lodash
setCommentList(_.orderBy(commentList, 'like', 'desc'));
} else {
// 根据创建时间排序
setCommentList(_.orderBy(commentList, 'ctime', 'desc'))
}
};
// 发表评论
const [content, setContent] = useState('');
const handlePublish = () => {
setCommentList([
...commentList,
{
rpid: uuidV4(),
user: {
uid: '30009257',
avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
uname: '黑马前端',
},
content: content,
ctime: dayjs(new Date()).format('MM-DD hh:mm'),
like: 80,
}
]);
// 1.清楚输入框内容
setContent('')
// 2.重新聚焦
inputRef.current.focus();
};
return (
<div className="app">
{/* 导航 Tab */}
<div className="reply-navigation">
<ul className="nav-bar">
<li className="nav-title">
<span className="nav-title-text">评论</span>
{/* 评论数量 */}
<span className="total-reply">{10}</span>
</li>
<li className="nav-sort">
{/* 高亮类名: active */}
{tabs.map(item =>
<span key={item.type}
onClick={() => handleTabChange(item.type)}
className={classNames('nav-item', {active: type === item.type})}>
{item.text}
</span>)
}
</li>
</ul>
</div>
<div className="reply-wrap">
{/* 发表评论 */}
<div className="box-normal">
{/* 当前用户头像 */}
<div className="reply-box-avatar">
<div className="bili-avatar">
<img className="bili-avatar-img" src={avatar} alt="用户头像" />
</div>
</div>
<div className="reply-box-wrap">
{/* 评论框 */}
<textarea
className="reply-box-textarea"
placeholder="发一条友善的评论"
value={content}
onChange={(e) => setContent(e.target.value)}
ref={inputRef}
/>
{/* 发布按钮 */}
<div className="reply-box-send">
<div className="send-text" onClick={handlePublish}>发布</div>
</div>
</div>
</div>
{/* 评论列表 */}
<div className="reply-list">
{/* 评论项 */}
{commentList.map(item => (
<div key={item.rpid} className="reply-item">
{/* 头像 */}
<div className="root-reply-avatar">
<div className="bili-avatar">
<img
className="bili-avatar-img"
alt=""
src={item.user.avatar}
/>
</div>
</div>
<div className="content-wrap">
{/* 用户名 */}
<div className="user-info">
<div className="user-name">{item.user.uname}</div>
</div>
{/* 评论内容 */}
<div className="root-reply">
<span className="reply-content">{item.content}</span>
<div className="reply-info">
{/* 评论时间 */}
<span className="reply-time">{item.ctime}</span>
{/* 评论数量 */}
<span className="reply-time">点赞数:{item.like}</span>
{/*条件:user.id === item.user.id*/}
{user.uid === item.user.uid && <span className="delete-btn" onClick={() => handleDel(item.rpid)}>删除</span>}
</div>
</div>
</div>
</div>
))}
</div>
</div>
</div>
)
}
export default App;
封装评论项 Item 组件
import './App.scss'
import avatar from './images/bozai.png'
import {useEffect, useRef, useState} from "react";
import _ from 'lodash';
import classNames from "classnames";
import {v4 as uuidV4} from 'uuid'
import dayjs from "dayjs";
import axios from "axios";
/**
* 评论列表的渲染和操作
*
* 1. 根据状态渲染评论列表
* 2. 删除评论
*/
// 评论列表数据
const list = [
{
// 评论id
rpid: 3,
// 用户信息
user: {
uid: '13258165',
avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
uname: '周杰伦',
},
// 评论内容
content: '哎哟,不错哦',
// 评论时间
ctime: '10-20 08:15',
like: 38,
},
{
rpid: 2,
user: {
uid: '36080105',
avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
uname: '许嵩',
},
content: '我寻你千百度 日出到迟暮',
ctime: '09-13 11:29',
like: 88,
},
{
rpid: 1,
user: {
uid: '30009257',
avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
uname: '黑马前端',
},
content: '学前端就来黑马',
ctime: '10-19 09:00',
like: 66,
},
]
// 当前登录用户信息
const user = {
// 用户id
uid: '30009257',
// 用户头像
avatar,
// 用户昵称
uname: '黑马前端',
}
/**
* 导航 Tab 的渲染和操作
*
* 1. 渲染导航 Tab 和高亮
* 2. 评论列表排序
* 最热 => 喜欢数量降序
* 最新 => 创建时间降序
*/
// 导航 Tab 数组
const tabs = [
{ type: 'hot', text: '最热' },
{ type: 'time', text: '最新' },
]
// 封装请求数据的 Hook
function useGetList() {
// 获取接口数据渲染
const [commentList, setCommentList] = useState([]);
useEffect( () => {
// 请求数据
async function getList() {
// axios 请求数据
const res = await axios.get('http://localhost:3004/list');
setCommentList(res.data);
}
getList();
}, []);
return {
commentList,
setCommentList
};
}
// 评论项组件
function Item({ item, onDel }) {
return (
<div className="reply-item">
{/* 头像 */}
<div className="root-reply-avatar">
<div className="bili-avatar">
<img
className="bili-avatar-img"
alt=""
src={item.user.avatar}
/>
</div>
</div>
<div className="content-wrap">
{/* 用户名 */}
<div className="user-info">
<div className="user-name">{item.user.uname}</div>
</div>
{/* 评论内容 */}
<div className="root-reply">
<span className="reply-content">{item.content}</span>
<div className="reply-info">
{/* 评论时间 */}
<span className="reply-time">{item.ctime}</span>
{/* 评论数量 */}
<span className="reply-time">点赞数:{item.like}</span>
{/*条件:user.id === item.user.id*/}
{user.uid === item.user.uid && <span className="delete-btn" onClick={() => onDel(item.rpid)}>删除</span>}
</div>
</div>
</div>
</div>
)
}
const App = () => {
// 渲染评论列表
// 1.使用 useState 维护 list
// const [commentList, setCommentList] = useState(_.orderBy(list, 'like', 'desc'));
const { commentList, setCommentList } = useGetList();
const inputRef = useRef(null);
// 删除功能
const handleDel = (id) => {
// 对 commentList 进行过滤
setCommentList(commentList.filter(item => item.rpid !== id));
};
// tab 切换功能
// 1.点击谁就把谁的 type 记录下来
// 2.通过记录 type 和每一项遍历时的 type 做匹配 控制激活类名的显示
const [type, setType] = useState('hot');
const handleTabChange = (type) => {
console.log(type);
setType(type);
// 基于列表的排序
if (type === 'hot') {
// 根据点赞数量排序
// lodash
setCommentList(_.orderBy(commentList, 'like', 'desc'));
} else {
// 根据创建时间排序
setCommentList(_.orderBy(commentList, 'ctime', 'desc'))
}
};
// 发表评论
const [content, setContent] = useState('');
const handlePublish = () => {
setCommentList([
...commentList,
{
rpid: uuidV4(),
user: {
uid: '30009257',
avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
uname: '黑马前端',
},
content: content,
ctime: dayjs(new Date()).format('MM-DD hh:mm'),
like: 80,
}
]);
// 1.清楚输入框内容
setContent('')
// 2.重新聚焦
inputRef.current.focus();
};
return (
<div className="app">
{/* 导航 Tab */}
<div className="reply-navigation">
<ul className="nav-bar">
<li className="nav-title">
<span className="nav-title-text">评论</span>
{/* 评论数量 */}
<span className="total-reply">{10}</span>
</li>
<li className="nav-sort">
{/* 高亮类名: active */}
{tabs.map(item =>
<span key={item.type}
onClick={() => handleTabChange(item.type)}
className={classNames('nav-item', {active: type === item.type})}>
{item.text}
</span>)
}
</li>
</ul>
</div>
<div className="reply-wrap">
{/* 发表评论 */}
<div className="box-normal">
{/* 当前用户头像 */}
<div className="reply-box-avatar">
<div className="bili-avatar">
<img className="bili-avatar-img" src={avatar} alt="用户头像" />
</div>
</div>
<div className="reply-box-wrap">
{/* 评论框 */}
<textarea
className="reply-box-textarea"
placeholder="发一条友善的评论"
value={content}
onChange={(e) => setContent(e.target.value)}
ref={inputRef}
/>
{/* 发布按钮 */}
<div className="reply-box-send">
<div className="send-text" onClick={handlePublish}>发布</div>
</div>
</div>
</div>
{/* 评论列表 */}
<div className="reply-list">
{/* 评论项 */}
{commentList.map(item => <Item item={item} onDel={handleDel} />)}
</div>
</div>
</div>
)
}
export default App;
17. Redux 集中状态管理工具
Redux 是 React 最常用的集中状态管理工具 ,类似于 Vue 中的 Pinia(Vuex),可以独立于框架运行
作用:通过集中管理的方式管理应用的状态
使用步骤:
-
定义一个 Redux 函数
-
使用 createStore 方法传入 reducer 函数,生成一个 store 实例对象
-
使用 store 实例的 subscribe 方法订阅数据的变化(数据一旦变化,可以得到通知)
-
使用 store 实例的 dispatch方法提交 action 对象触发数据变化(告诉 reducer 你想怎么改数据)
-
使用 store 实例的 getState 方法获取最新的状态数据更新到视图中
Redux 与 React环境准备
配套工具
-
Redux Toolkit --- 官方推荐编写 Redux 逻辑的方式,是一套工具的集合,简化书写方式
-
简化 store 配置
-
内置 immer 支持可变式状态修改
-
内置 thunk,更好的异步创建
-
-
react-redux --- 用来链接 Redux 和 React 组件的中间件
- 获取状态,更新状态
配置基础环境
- 使用CRA快速创建React项目
npx create-react-app react-redux
- 安装配套工具
npm i @reduxjs/toolkit react-redux
- 启动项目
npm run start
使用 React Toolkit 创建 counterStore
counterStore.js:
import {createSlice} from "@reduxjs/toolkit";
const counterStore = createSlice({
// store 名称
name: 'counter',
// 初始化状态
initialState: {
count: 0
},
// 修改状态的方法,同步方法,支持直接修改
reducers: {
increment(state) {
state.count++;
},
decrement(state) {
state.count--;
}
}
});
// 结构出来 actionCreator 函数
const {increment, decrement} = counterStore.actions;
const reducer = counterStore.reducer;
// 按需导出 actionCreator
export {increment, decrement};
// 默认导出 reducer
export default reducer;
src\store\index.js:
import {configureStore} from "@reduxjs/toolkit";
import counterStore from "./modules/counterStore";
const store = configureStore({
reducer: {
counter: counterStore
}
});
export default store;
src\index.js:
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import {Provider} from "react-redux";
import store from "./store";
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
React 组件使用 store 中的数据
const { count } = useSelector(state => state.counter);
React 组件修改 store 中的数据
import './App.css';
import {useDispatch, useSelector} from "react-redux";
// 导入 actionCreator
import {decrement, increment} from "./store/modules/counterStore";
function App() {
const { count } = useSelector(state => state.counter);
const dispatch = useDispatch();
return (
<div className="App">
<button onClick={() => dispatch(decrement())}>-</button>
<span>{count}</span>
<button onClick={() => dispatch(increment())}>+</button>
</div>
);
}
export default App;
总结:
提交 action 修改 state 值
counterStore.js:
import {createSlice} from "@reduxjs/toolkit";
const counterStore = createSlice({
// store 名称
name: 'counter',
// 初始化状态
initialState: {
count: 0
},
// 修改状态的方法,同步方法,支持直接修改
reducers: {
increment(state) {
state.count++;
},
decrement(state) {
state.count--;
},
addToNum(state, action) {
state.count += action.payload;
}
}
});
// 结构出来 actionCreator 函数
const {increment, decrement, addToNum} = counterStore.actions;
const reducer = counterStore.reducer;
// 按需导出 actionCreator
export { increment, decrement, addToNum };
// 默认导出 reducer
export default reducer;
App.js:
import './App.css';
import {useDispatch, useSelector} from "react-redux";
// 导入 actionCreator
import {addToNum, decrement, increment} from "./store/modules/counterStore";
function App() {
const { count } = useSelector(state => state.counter);
const dispatch = useDispatch();
return (
<div className="App">
<button onClick={() => dispatch(decrement())}>-</button>
<span>{count}</span>
<button onClick={() => dispatch(increment())}>+</button>
<button onClick={() => dispatch(addToNum(10))}>add to 10</button>
<button onClick={() => dispatch(addToNum(20))}>add to 20</button>
</div>
);
}
export default App;
Redux 异步状态操作
异步操作样板:
-
创建stor的写法保持不变,配置好同步修改状态的方法
-
单独封装一个函数,在函数内部return一个新函数,在新函数中
-
封装异步请求获取数据
-
调用同步action Creater传入异步数据生成一个action对象,并使用dispatch提交
-
-
组件中dispatch的写法保特不变
channelStore.js:
import {createSlice} from "@reduxjs/toolkit";
import axios from "axios";
const channelStore = createSlice({
// store 名称
name: 'channel',
initialState: {
channelList: []
},
reducers: {
setChannels(state, action) {
state.channelList = action.payload;
}
}
});
// 异步请求部分
const { setChannels } = channelStore.actions;
const fetchChannelList = () => {
return async (dispatch) => {
const res = await axios.get('http://geek.itheima.net/v1_0/channels');
dispatch(setChannels(res.data.data.channels));
};
};
export { fetchChannelList };
const reducer = channelStore.reducer;
export default reducer;
index.js:
import {configureStore} from "@reduxjs/toolkit";
import counterStore from "./modules/counterStore";
import channelStore from "./modules/channelStore";
const store = configureStore({
reducer: {
counter: counterStore,
channel: channelStore,
}
});
export default store;
App.js:
import {useDispatch, useSelector} from "react-redux";
// 导入 actionCreator
import {addToNum, decrement, increment} from "./store/modules/counterStore";
import {useEffect} from "react";
import {fetchChannelList} from "./store/modules/channelStore";
function App() {
const { count } = useSelector(state => state.counter);
const { channelList } = useSelector(state => state.channel);
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchChannelList());
}, [dispatch]);
return (
<div className="App">
<button onClick={() => dispatch(decrement())}>-</button>
<span>{count}</span>
<button onClick={() => dispatch(increment())}>+</button>
<button onClick={() => dispatch(addToNum(10))}>add to 10</button>
<button onClick={() => dispatch(addToNum(20))}>add to 20</button>
<ul>
{channelList.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
</div>
);
}
export default App;
18. React Router 路由
上手 demo
需求:创建一个可以切换登录和文章页的路由
实现:
npm i react-router-dom # 安装路由依赖
index.js:
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import reportWebVitals from './reportWebVitals';
import {createBrowserRouter, RouterProvider} from "react-router-dom";
// 1. 创建 Router 实例对象并且配置路由对应关系
const router = createBrowserRouter([
{
path: '/login',
element: <div>我是登录页</div>
},
{
path: '/article',
element: <div>我是文章页</div>
},
]);
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<RouterProvider router={router}></RouterProvider>
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
实际开发中的 router 配置
1.创建 page 文件夹,分别创建 Login 和 Article 目录,再分别创建 index.js 文件
src/page/article/index.js:
const Article = () => {
return <div>我是文章</div>
};
export default Article;
src/page/login/index.js:
const Login = () => {
return <div>我是登录</div>
};
export default Login;
2.创建 router 文件夹,在其中创建 index.js 文件
router/index.js:
import {createBrowserRouter} from "react-router-dom";
import Login from "../page/login";
import Article from "../page/article";
const router = createBrowserRouter([
{
path: '/login',
element: <Login />
},
{
path: '/article',
element: <Article />
}
]);
export default router;
3.在 src/index.js 文件中引入 router 实例
src/index.js:
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import reportWebVitals from './reportWebVitals';
import {createBrowserRouter, RouterProvider} from "react-router-dom";
import router from './router';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<RouterProvider router={router}></RouterProvider>
</React.StrictMode>
);
reportWebVitals();
路由导航
声明式导航
指的是在模板中通过 <Link />
组件描述要调到哪里去,常用于 Tab 栏。类似 Vue 的 router-link
示例:
<Link to="/article">文章</Link>
编程式导航
编程式导航是指通过useNavigate
钩子得到导航方法,然后通过调用方法以命令式的形式进行路由跳转,比如想在 登录请求完毕之后跳转就可以选择这种方式,更加灵活
示例:
import {Link, useNavigate} from "react-router-dom";
const Login = () => {
const navigate = useNavigate();
return (
<div>
<div>我是登录</div>
<Link to='/article'>跳转文章页</Link>
<button onClick={() => navigate('/article')}>跳转文章页</button>
</div>
)
};
export default Login;
路由传参
searchParams 传参
// 传参
const navigate = useNavigate();
<button onClick={() => navigate('/article?id=1001&name=jack')}>searchParams 传参</button>
// 拿参
const [params] = useSearchParams();
const id = params.get('id');
<div>我是文章页{id}</div>
params 传参
// 传参
const navigate = useNavigate();
<button onClick={() => navigate('/article/1001')}>params 传参</button>
// 取参
const params = useParams();
const id = params.id;
<div>我是文章页{id}</div>
注意 parmas 传参必须要在 router/index.js 文件的 path 加上 :id
import {createBrowserRouter} from "react-router-dom";
import Login from "../page/login";
import Article from "../page/article";
const router = createBrowserRouter([
{
path: '/login',
element: <Login />
},
{
path: '/article/:id',
element: <Article />
}
]);
export default router;
传参多值:
// 传参
const navigate = useNavigate();
<button onClick={() => navigate('/article?id=1001&name=jack')}>searchParams 传参</button>
<button onClick={() => navigate('/article/1001/hejiajun')}>params 传参</button>
// 取参
const params = useParams();
const id = params.id;
const name = params.name;
<div>我是文章页{id}</div>
<div>我是文章页{name}</div>
router/index.js:
import {createBrowserRouter} from "react-router-dom";
import Login from "../page/login";
import Article from "../page/article";
const router = createBrowserRouter([
{
path: '/login',
element: <Login />
},
{
path: '/article/:id/:name',
element: <Article />
}
]);
export default router;
嵌套路由配置
大致步骤:
-
使用 children 属性配置路由嵌套关系
-
使用 <Outlet /> 组件配置二级路由渲染位置
router/index.js:
import {createBrowserRouter} from "react-router-dom";
import Login from "../page/login";
import Article from "../page/article";
import Layout from "../page/layout";
import Board from "../page/board";
import About from "../page/about";
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [
{
path: 'board',
element: <Board />
},
{
path: 'about',
element: <About />
}
]
},
{
path: '/login',
element: <Login />
},
{
path: '/article/:id/:name',
element: <Article />
}
]);
export default router;
src/page/layout/index.js:
import {Link, Outlet} from "react-router-dom";
const Layout = () => {
return (
<div>
我是一级路由 layout 组件
<Link to='/board'>面板</Link>
<Link to='/about'>关于</Link>
{/*配置二级路由的出口*/}
<Outlet />
</div>
)
};
export default Layout;
默认二级路由设置
当访问的是一级路由时,默认的二级路由组件可以得到渲染,只需要在二级路由的位置去掉path,设置 index 属性为 true
src/router/index.js
import {createBrowserRouter} from "react-router-dom";
import Login from "../page/login";
import Article from "../page/article";
import Layout from "../page/layout";
import Board from "../page/board";
import About from "../page/about";
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [
// 设置默认二级路由,一级路由被访问的时候,它也能得到渲染
{
index: true,
element: <Board />
},
{
path: 'about',
element: <About />
}
]
},
{
path: '/login',
element: <Login />
},
{
path: '/article/:id/:name',
element: <Article />
}
]);
export default router;
404 路由配置
场景:当浏览器输入url的路径在整个路由配置中都找不到对应的pth,为了用户体验,可以使用404兜底组件进行 渲染
两种路由模式
创建 hash 路由
import {createHashRouter} from "react-router-dom";
import Login from "../page/login";
import Article from "../page/article";
import Layout from "../page/layout";
import Board from "../page/board";
import About from "../page/about";
import NotFound from "../page/notfound";
const router = createHashRouter([
{
path: '/',
element: <Layout />,
children: [
// 设置默认二级路由,一级路由被访问的时候,它也能得到渲染
{
index: true,
element: <Board />
},
{
path: 'about',
element: <About />
}
]
},
{
path: '/login',
element: <Login />
},
{
path: '/article/:id/:name',
element: <Article />
},
{
path: '*',
element: <NotFound />
}
]);
export default router;
19. 记账本
这边没有认真记,想要学习的小伙伴可以直接看原视频好了,配合视频和笔记学习效果会更好。
环境搭建
使用CRA创建项目,并安装必要依赖,包括下列基础包
npx create-react-app react-bill
-
Redux:状态管理-@reduxjs./toolkit、react-redux
-
路由-reac-router-dom
-
时间处理-dayjs
-
class:类名处理-classnames
-
移动端组件库-antd-mobile
-
请求插件-axios
npm i @reduxjs/toolkit react-redux react-router-dom dayjs classnames antd-mobile axios
配置别名路径
1.路径解析配置,把 @/ 解析为 src/ (1. npm i -D @craco/craco
2.项目根目录下创建配置文件craco.config.js
)
2.路径联想配置(VsCode),VsCode在输入@/时,自动联想出来对应的src/下的子级目录
数据 Mock
npm i -D json-server
20. UseReducer
作用:和 useState 作用类似,用来管理相对复杂的状态数据
基础用法:
-
定义一个reducer函数(根据不同的action返回不同的新状态)
-
在组件中调用useReducer,并传入reducer函数和状态的初始值
-
事件发生时,通过 dispatch 函数分派一个 action 对象(通知 reducer 要返回哪个新状态并渲染 UI)
App.js:
import React, {useReducer} from "react";
// useReducer
// 1. 定义 reducer 函数,根据不同的action返回不同的新状态
// 2. 在组件中调用 useReducer,并传入 reducer 函数和状态的初始值
// 3. 调用 dispatch 通知 reducer 产生一个新状态,利用新状态更新 UI
function reducer(state, action) {
switch (action.type) {
case 'INC':
return state + 1;
case 'DEC':
return state - 1;
case 'SET':
return action.payload;
default:
return state;
}
}
function App() {
const [state, dispatch] = useReducer(reducer, 0);
return (
<div className="App">
this is app
<button onClick={() => dispatch({ type: 'DEC' })}>-</button>
{state}
<button onClick={() => dispatch({ type: 'INC' })}>+</button>
<button onClick={() => dispatch({ type: 'SET', payload: 100 })}>update</button>
</div>
);
}
export default App;
21. useMemo
需求:
作用:在组件每次渲染的时候缓存计算结果
useMemo(() => {
// 根据 count1 返回计算的结果
}, [count1])
说明:使用useMemo做缓存之后可以保证只有count1依赖项发生变化时才会重新计算。接受两个参数,第一个是函数,第二个是依赖项。
App.js:
import React, {useMemo, useState} from "react";
function fib(n) {
console.log('计算函数执行了');
if (n < 3) {
return 1;
}
return fib(n - 2) + fib(n - 1);
}
function App() {
const [count1, setCount1] = useState(0);
const result = useMemo(() => {
return fib(count1);
}, [count1]);
// const result = fib(count1);
const [count2, setCount2] = useState(0);
console.log('组件重新渲染了');
return (
<div className="App">
this is app
<button onClick={() => setCount1(count1 + 1)}>change count1: {count1}</button>
<button onClick={() => setCount2(count2 + 1)}>change count2: {count2}</button>
result is {result}
</div>
);
}
export default App;
使用场景:消耗非常大的计算