前言
什么是组件?
- 组件是一个广泛的概念,现在流行的框架中都有组件;
- 一个组件就是用户界面的一部分,它可以有自己 的 逻辑 和 外观,组件之间 可以 相互嵌套,也可以 复用 多次;
一、React组件
1.1 基本概念 及 注意事项
- 在 React 中,一个 组件 就是 首字母 大写 的 函数,内部存放了 组件的 逻辑 和 视图UI,渲染组件只需要把组件 当成 标签 书写 即可;
- ❗ 注意:
- React组件是常规的JS函数,但 组件的名称 必须以 大写字母 开头,否则它们将无法运行;
- React组件 必须 要有一个 根标签;
- 通常跟标签都是使用
<></>一对空标签表示的;- 但,有时候我们需要使用
map循环列表,需要添加key属性,在空标签上不能添加key属性,所以此时就需要使用<Fragment></Fragment>;<></>是对<Fragment></Fragment>的简写;- 它们都允许你在不添加额外节点的情况下将子元素组合;
- React组件函数也可以是箭头函数;
- 如果你的标签和
return关键字不在同一行,则必须把它包裹在一对括号中(小括号);
- 没有括号包裹的话,任何在
return下一行的代码 都将被忽略;- 所有的标签都必须是闭合标签;
- 如果是单标签:必须自闭合;
1.2 定义组件
// recat 17之前是要写的,17之后可以省略
import React from 'react';
// 1. 定义函数
function Button() {
// 组件内部逻辑
// 2. 添加标签
return <button> click me </button>
}
// 3. 导出组件
export default Button;
1.3 使用组件(渲染组件)
- 正常的定义组件都是以上三个步骤,这里为了简单方便,就直接在 App.js 中定义组件;
// 定义组件
const Button = () => {
// 业务逻辑 及 组件逻辑
const onClick = (name, e) => {
console.log(name, e);
};
return <button onClick={(e) => onClick('禁止摆烂_才浅', e)}>Click Me</button>;
}
// 使用组件
function App() {
return (
<div>
{/* 单标签 ===> 自闭合 */}
<Button />
{/* 双标签 */}
<Button></Button>
</div>
);
}
export default App;
二、useState 基础使用
2.1 基本介绍
useState是一个 React Hook(函数),它允许我们向组件添加一个 状态变量,从而控制影响组件的渲染结果;- ❗ 本质:
- 和普通JS变量不同的是,状态变量一旦发生变化,组件的视图UI也会跟着变化(数据驱动试图);
- 就比如说,我将 状态变量 count 的值 从 0 => 1,那么视图上的显示结果也会从 0 => 1;
2.2 语法
- ❗ 注意:
- 使用之前需要先导入;
- React Hooks 必须在 React函数组件 或 自定义Hook函数 的最顶层 调用;
import { useState } from 'react';
const [状态变量, 修改状态变量的函数] = useState(初始值);
// const [状态变量, set状态变量] = useState(初始值);
- 作用:
- 向组件添加一个状态变量;
- 语法:
- 在 组件顶层 调用
useState来声明一个状态变量; - 使用 数组解构 来命名状态变量;
import { useState } from 'react'; const App = () => { const [num, setNum] = useState(初始值); }; export default App; - 在 组件顶层 调用
- useState() 参数(初始值):
- 任何类型的数据;
useState的 参数 将作为 状态变量 的 初始值;- ❗ 注意:
- 如果传递 函数 作为初始值,则它将被视为 初始化函数;
- 该函数一定是一个 纯函数,不应该 接受 任何参数,并且应该返回一个任何类型的值;
- 当初始化组件时,React将 调用该初始化函数,并将其 返回值 存储为 初始状态;
- useState() 返回值:
- 返回一个由 两个值 组成的 数组;
- 当前的state:
- 在首次渲染时,它将与你传递的初始值相同;
- set函数:
- 它可以让你将state更新为不同的值,并触发重新渲染;
- 给这个
set函数传递什么,对应的state就会更新成什么; set函数没有返回值;
useState是一个函数,返回值 是 由两个值组成的 数组;- 代码展示:
import { useState } from 'react'; const Button = () => { // 调用 useState 添加一个状态变量 // num ===> 状态变量 // setNum ===> 修改状态变量的函数 const [num, setNum] = useState(0); // 点击事件 - num自增 const onClick = () => { // 调 setNum 的作用 // 1. 修改 状态变量 num 的值(用传入的新值修改状态变量) // 2. 重新使用新的 状态变量 num 渲染视图 setNum(num + 1); }; return ( <div> <button onClick={onClick}>Click Me</button> <br /> <span>状态变量的值 --- {num}</span> </div> ); }; function App() { return ( <div> <Button /> </div> ); } export default App;
2.3 修改状态的规则
2.3.1 修改 基本数据类型 的状态
- 在 React 中,状态 被认为是 只读 的,我们应该始终 替换 而不是 修改 它,直接修改 状态 不能引起视图更新;
- ❗ 注意:
- 直接修改 状态变量 的 值,状态变量 能被 修改,但是 不会引起 视图 的 更新;
- 既要 修改状态 变量的值,还想要 视图同时更新,只能通过
useState的set函数去修改状态变量的值;
- 代码展示:
import { useState } from 'react'; const Button = () => { // 调用 useState 添加一个状态变量 // num ===> 状态变量 // setNum ===> 修改状态变量的函数 let [num, setNum] = useState(0); // 点击事件 - num自增 const onClick = () => { setNum(num + 1); }; const onClick1 = () => { num++; }; return ( <div> <button onClick={onClick1}>Click Me</button> <br /> <span>直接修改状态变量的值 --- {num}</span> <br /> <hr /> <br /> <button onClick={onClick}>Click Me</button> <br /> <span>使用useState的set函数状态变量的值 --- {num}</span> </div> ); }; function App() { return ( <div> <Button /> </div> ); } export default App; - 演示效果:
2.3.2 修改 对象、数组 的状态
- 规则:
- 对于对象类型的状态变量,应该始终传给
set一个 全新的对象 来进行替换;
- 对于对象类型的状态变量,应该始终传给
- 代码展示:
import { useState } from 'react'; const Button = () => { // 对象格式 const [info, setInfo] = useState({ name: '张三', age: 22, gender: '男' }); const onClick = () => { setInfo({ ...info, name: '李四', age: 58 }); }; const onClick1 = () => { info.name = '王麻子'; info.age = 44; console.log(info); }; // 数组形式 const [numArr, setNumArr] = useState([0, 1, 2, 3, 4]); const onChange = () => { setNumArr([1, 2, 3, 4, 5]); }; const onChange1 = () => { numArr[0] = 100; console.log(numArr); }; return ( <div> <button onClick={onClick1}>Click Me</button> <br /> <span> 直接修改状态变量的值 --- {info.name} - {info.age} </span> <br /> <hr /> <br /> <button onClick={onClick}>Click Me</button> <br /> <span> 使用useState的set函数状态变量的值 --- {info.name} - {info.age} </span> <br /> <hr /> <br /> <button onClick={onChange1}>Click Me</button> <br /> <span>直接修改状态变量的值 --- {numArr}</span> <br /> <hr /> <br /> <button onClick={onChange}>Click Me</button> <br /> <span>使用useState的set函数状态变量的值 --- {numArr}</span> </div> ); }; function App() { return ( <div> <Button /> </div> ); } export default App; - 演示效果:
2.4 注意事项
- ❗ 注意:
useState是一个Hook,因此只能在 组件的顶层 或 自己的Hook 中调用它;- 不能在循环或条件语句中调用它;
- 如果非要这样使用,需要提取一个新组件并将状态移入其中;
- 在严格模式中,React将 两次调用初始化函数,使用其中的一个值而忽略另一个值;
set函数 仅更新 下一次 渲染的状态变量。如果在调用set函数后读取状态变量,则仍然得到的是调用之前的旧值;
- 不要在更新state的函数中使用state,因为此时的state还是上一次的state,并不是最新的;
import { useState } from 'react'; const App = () => { const [num, setNum] = useState(0); const onUpdateNum = () => { setNum(num + 1); console.log(num); // 0,此时的num还是组件初始化时的num,并不是最新的 }; return <button onClick={onUpdateNum}>state:{ num }</button> }; export deault App;- react state 更新机制:
- 当
react中的state更新的时候,整个组件函数里面的所有代码都会重新执行;
三、组件的样式处理
- React组件基础的样式控制有两种控制方案:
- ✅ class类名控制(单独写样式,导入到组件文件中);
- ❌ 行内样式(极不推荐);
3.1 ✅ class类名控制
3.1.1 固定类名
- 代码展示:
.box { width: 100px; height: 100px; background-color: red; }import './App.css'; const App = () => { return <div className="box box1"></div>; }; export default App;
- ❗ 注意:
- 在 React 中,使用类名的时候,需要使用
className关键字 替代之前的class;
3.1.2 动态 添加 或 删除 类名
动态判断添加 单类名
<div className={item.readState === 0 ? 'no-read' : null}></div>
已有多类名,动态判断再添加类型
// 数组方法
<div className={['box', classA, item.readState === 0 ? 'no-read' : null].join(' ')}></div>
<div className={['box', classA, item.readState === 0 && 'no-read'].join(' ')}></div>
// 模板字符串方法
<div className={`box ${classA} ${item.readState === 0 ? 'no-read' : null}`}></div>
<div className={`box ${classA} ${item.readState === 0 && 'no-read}`}></div>
- ❗ 注意:
- 数组方法时:
- 要使用 空格 将数组转为字符串;
- 模板字符串方法时:
- 类名之间 必须要有 空格;
✅ 使用 classnames 依赖
在实际开发中,我们通常需要根据某个条件去判断类名,此时我们可以使用
classnames这个第三方包进行设置;
// 安装依赖
npm i classnames
import classNames from 'classnames';
<div className={classNames('box', {'no-read': item.readState === 0 })}></div>
3.2 ❌ 行内样式
- 行内样式有两种方案:
- 直接将样式写在行内;
- 将样式属性写在一个对象中,将这个对象绑定到对应的元素上;
- 代码展示:
- 将样式写在行内:
const App = () => { return <div style={{ width: '100px', height: '100px', backgroundColor: 'red' }}>Hello World</div>; }; export default App; - 使用对象:
const style = { width: '100px', height: '100px', backgroundColor: 'red', color: '#fff' }; const App = () => { return <div style={style}>Hello World</div>; }; export default App;
- 将样式写在行内:
四、受控表单组件绑定
4.1 概念
- 使用 React 组件状态的状态(
useState)控制表单的状态;
4.2 使用步骤
- 准备一个React状态值
- 使用
useState声明状态;
const [value, setValue] = useState(''); - 使用
- 通过
value属性绑定状态,通过onChange事件绑定状态同步的函数,通过事件对象e拿到输入框最新的值,反向修改react的状态;<input type="text" value={value} onChange={(e) => setValue(e.target.value)}/>
- ❗ 注意:
- 当给表单元素设置
value属性的时候,这个字段呈现一个 只读字段;- 如果该字段应该是可变的,则使用
defaultValue;- 否则,设置
onChange或readOnly;
五、获取DOM元素
- 在 React 中 获取 / 操作 DOM,需要使用
useRef钩子函数,分为两步:- 使用
useRef创建ref对象,并于 JSX 绑定;- 在组建内部调用;
const inputRef = useRef(null); <input type="text" ref={inputRef} />
- 使用
- 在DOM可用时,通过
ref名称.current拿到DOM对象;console.log(inputRef.current);
- ❗ 什么是DOM可用?
- 组件渲染完毕之后才可用的;
- 渲染之前获取DOM得到的是
null;
六、案例展示 - B站评论 - 普通版
- 效果展示:
- 功能需求:
- 渲染评论列表;
- 实现删除评论;
- 只有自己的评论才显示删除按钮;
- 点击删除按钮,删除当前评论,列表中不再显示;
- 渲染导航Tab和高亮实现;
- 评论列表排序功能实现;
- 最新:评论列表按照创建时间排序(新的在前);
- 最热:点赞数排序(点赞数多的在前);
- 实现评论功能;
- 点击发布之后,需要清空输入框的内容,并且自动获取焦点;
- 回车也可以发送评论;
- 核心思路:
- 使用
useState维护评论列表;- 使用
map对列表进行遍历渲染(一定要添加key);
- 代码展示:
import { useRef, useState } from 'react';
// 需要安装 lodash
import _ from 'lodash';
// 导入所需样式 - 放在本文的末尾
import './bilibili-review.scss';
// 大家自己在本地虽败找一张图片
import avatar from './images/bozai.png';
// 评论列表数据
const defaultList = [
{
// 评论id
rpid: 3,
// 用户信息
user: {
uid: '13258165',
avatar: 'https://yjy-teach-oss.oss-cn-beijing.aliyuncs.com/reactbase/comment/zhoujielun.jpeg',
uname: '周杰伦'
},
// 评论内容
content: '哎哟,不错哦',
// 评论时间
ctime: '10-18 08:15',
// 喜欢数量
like: 98,
// 0:未表态 1: 喜欢 2: 不喜欢
action: 0
},
{
rpid: 2,
user: {
uid: '36080105',
avatar: 'https://yjy-teach-oss.oss-cn-beijing.aliyuncs.com/reactbase/comment/xusong.jpeg',
uname: '许嵩'
},
content: '我寻你千百度 日出到迟暮',
ctime: '11-13 11:29',
like: 88,
action: 2
},
{
rpid: 1,
user: {
uid: '30009257',
avatar,
uname: '黑马前端'
},
content: '学前端就来黑马',
ctime: '10-19 09:00',
like: 66,
action: 1
}
];
// 当前登录用户信息
const user = {
// 用户id
uid: '30009257',
// 用户头像
avatar,
// 用户昵称
uname: '黑马前端'
};
// 头部Tab配置项
const tabOptions = [
{ type: 'latest', text: '最新' },
{ type: 'hottest', text: '最热' }
];
const App = () => {
// 评论列表数据
const newList = _.orderBy(defaultList, ['ctime'], ['desc']);
const [list, setList] = useState(newList);
// 记录活跃的Tab状态
const [activeTab, setActiveTab] = useState('latest');
/** 更新评论列表 - 删除 + 排序 */
const updateList = (type, id) => {
let newList = [];
if (type && !['del'].includes(type)) setActiveTab(type);
switch (type) {
case 'del':
newList = list.filter((item) => item.rpid !== id);
break;
case 'latest':
newList = _.orderBy(list, ['ctime'], ['desc']);
break;
case 'hottest':
newList = _.orderBy(list, ['like'], ['desc']);
break;
default:
newList = list;
break;
}
setList(newList);
};
/** 保存输入框内容 */
const [value, setValue] = useState('');
/** 输入框ref */
const inputRef = useRef(null);
/** 发布评论 */
const addReview = (e) => {
if (e.key === 'Enter') e.preventDefault();
if (!value || (e.type === 'keydown' && e.key !== 'Enter')) return;
const item = {
rpid: new Date().getTime(),
user,
content: value,
ctime: new Date().toLocaleDateString(),
like: 0,
action: 0
};
setList([item, ...list]);
setValue('');
inputRef.current.focus();
};
return (
<div className="app">
{/* 头部 */}
<div className="header align-center">
<div className="title align-center">
评论<em>{list.length}</em>
</div>
{/* tab栏 */}
<ul className="tab align-center">
{tabOptions.map((item, index) => {
return (
// className={['item', 'align-center', activeTab === item.type ? 'active' : null].join(' ')}
// className={['item', 'align-center', activeTab === item.type && 'active'].join(' ')}
// className={`item align-center ${activeTab === item.type ? 'active' : null}`}
// className={`item align-center ${activeTab === item.type && 'active'}`}
<li
className={`item align-center ${activeTab === item.type && 'active'}`}
onClick={() => updateList(item.type)}
key={item.type}>
<span>{item.text}</span>
{tabOptions.length - 1 !== index && <span className="split-line"></span>}
</li>
);
})}
</ul>
</div>
<div className="review-box">
{/* 评论框 */}
<div className="post-review align-center">
<img src={user.avatar} alt={user.uname} className="avatar" />
<div className="input align-center">
<textarea
type="text"
ref={inputRef}
value={value}
onChange={(e) => setValue(e.target.value)}
onKeyDown={addReview}
placeholder="发一条友善的评论"
/>
<button onClick={addReview}>发布</button>
</div>
</div>
{/* 评论列表 */}
<div className="review-list">
{list.map(({ user: { avatar, uname, uid }, content, ctime, like, rpid }) => {
return (
<div className="review-item" key={rpid}>
<div className="left">
<img src={avatar} alt={uname} />
</div>
<div className="right">
<div className="user-name">{uname}</div>
<div className="content">{content}</div>
<div className="bottom">
<div className="time">{ctime}</div>
<div className="like-num">点赞数:{like}</div>
<ul className="controls">
{/* 只有自己的评论才展示删除按钮 */}
{uid === user.uid && (
<li className="del" onClick={() => updateList('del', rpid)}>
删除
</li>
)}
</ul>
</div>
</div>
</div>
);
})}
</div>
</div>
</div>
);
};
export default App;
- bilibili-review.scss
* { margin: 0; padding: 0; box-sizing: border-box; } body { background-color: #c9ccd0; } li { list-style: none; } em { font-style: normal; } .align-center { display: flex; align-items: center; } .app { padding: 100px; background-color: #fff; .header { justify-content: flex-start; height: 24px; .title { font-weight: bold; font-size: 18px; color: #333; em { margin-left: 10px; color: #666; font-size: 12px; font-weight: 400; } } .tab { justify-content: flex-start; height: 100%; margin-left: 30px; color: #666; font-size: 12px; li.item { cursor: pointer; &:hover { color: #000; } .split-line { height: 11px; margin: 0 12px; border-right: 1px solid #9499a0; } } .active { color: #00aeec; } } } .review-box { width: 100%; padding-left: 20px; .post-review { align-items: flex-start; width: 100%; height: 70px; margin-top: 16px; img { width: 40px; height: 40px; border-radius: 50%; margin-right: 16px; } .input { width: 100%; align-items: flex-start; textarea { width: calc(100% - 100px - 10px) !important; height: 50px; margin-right: 10px; padding-left: 10px; border: 1px solid #f1f2f3; background-color: #f1f2f3; border-radius: 6px; font-size: 16px; line-height: 50px; outline: none; resize: none; transition: height 0.2s; appearance: none; -webkit-appearance: none; &:hover, &:focus { border-color: #c9ccd0; background-color: #fff; } &:focus { height: 70px; } &::placeholder { font-size: 12px; } &::-webkit-scrollbar { display: none; } } button { width: 100px; height: 50px; background-color: #00aeec; border: none; border-radius: 6px; color: #fff; font-size: 15px; cursor: pointer; opacity: 0.5; &:hover { opacity: 1; } } } } .review-list { .review-item { display: flex; align-items: flex-start; width: 100%; margin-top: 20px; .left { width: 56px; height: 100%; img { width: 40px; height: 40px; border-radius: 50%; cursor: pointer; } } .right { width: calc(100% - 56px); height: 100%; padding-bottom: 20px; border-bottom: 2px solid #eee; .user-name { color: #61666d; font-size: 13px; cursor: pointer; } .content { margin: 16px 0 10px; font-size: 15px; } .bottom { display: flex; align-items: center; justify-content: flex-start; color: #9499a0; font-size: 13px; .like-num { margin: 0 20px; } .controls { li { cursor: pointer; } .del:hover { color: #000; } } } } } } } }