useState
在React中,
useState
是一个非常重要的Hook,它允许你在函数组件中添加“状态”(state)。在传统的React类组件中,我们使用this.state
来管理和更新组件的状态。然而,在函数组件中,由于它们没有实例,因此我们不能直接使用this.state
。这时,useState
Hook 就派上了用场。
在函数组件中,你可以使用useState
来声明一个新的状态变量。useState
接收一个初始状态值作为参数,并返回一个状态变量数组。数组的第一个元素是当前的状态值,第二个元素是一个用于更新状态的函数。
function ExampleComponent() {
// 声明一个新的状态变量,初始值为 0
const [count, setCount] = useState(0);
// 渲染一个按钮,点击时增加计数器的值
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
状态更新是异步的
需要注意的是,React可能会将多个setState
调用合并成一个批处理更新,以提高性能。因此,你不应该依赖于setState
的立即结果来计算下一个状态。例如,以下代码可能不会按预期工作:
setCount(count + 1);
console.log(count); // 这可能仍然打印旧的count值
如果你需要根据前一个状态来计算新的状态,你可以将函数作为setCount
的参数传递,该函数接收当前的状态值并返回新的状态值:
setCount(prevCount => prevCount + 1);
总结
useState
是React的一个Hook,允许你在函数组件中添加状态。useState
接收一个初始状态值作为参数,并返回一个包含当前状态值和更新状态函数的数组。- 状态更新可能是异步的,因此你不应该依赖于
setState
的立即结果。- 如果你需要根据前一个状态来计算新的状态,可以将函数作为
setState
的参数传递。
JSX中使用JS表达式
在JSX中可以通过大括号语法}识别JavaScript中的表达式,比如常见的变量、函数调用、方法调
用等等
1.使用引号传递字符串 2.使用JavaScript变量
3.函数调用和方法调用 4.使用JavaScript对象
//列表渲染
const items = ['Apple', 'Banana', 'Cherry'];
function ListItems() {
return (
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
);
}
//渲染条件内容:
function Greeting(props) {
const isLoggedIn = props.isLoggedIn;
let content;
if (isLoggedIn) {
content = <p>Welcome back!</p>;
} else {
content = <p>Please log in.</p>;
}
return (
<div>
{content}
</div>
);
}
-------------------------------------------------
function Greeting(props) {
return (
<div>
{props.isLoggedIn ? <p>Welcome back!</p> : <p>Please log in.</p>}
</div>
);
}
React基础事件绑定与传参
在React中,事件绑定是通过在JSX元素上设置事件处理属性来完成的。这些事件处理属性通常以on
开头,后跟事件名称(例如,onClick
、onChange
、onMouseMove
等),并且它们的值是一个函数。当特定事件在DOM元素上触发时,该函数将被调用。
import React from 'react';
class ButtonComponent extends React.Component {
handleClick = () => {
alert('Button was clicked!');
}
render() {
return (
<button onClick={this.handleClick}>
Click Me
</button>
);
}
}
export default ButtonComponent;
在上面的例子中,
ButtonComponent
类组件有一个handleClick
方法,该方法在按钮被点击时会被调用。我们通过在JSX中的button
元素上设置onClick
属性,并将handleClick
方法作为该属性的值来绑定事件。请注意,我们在
handleClick
方法前使用了箭头函数语法(handleClick = () => { ... }
),这是类属性(class fields)语法的一部分,它允许我们在类体中直接定义方法。箭头函数确保this
在handleClick
方法内部指向当前的类实例,而不是undefined
(在非箭头函数中,如果在类的方法中不使用bind
,this
通常会指向undefined
,除非在构造函数中绑定了this
)。
- 类组件中的方法(Class Component Methods):如果你不使用类属性语法,你也可以在构造函数中绑定方法:
import React from 'react';
class ButtonComponent extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
alert('Button was clicked!');
}
render() {
return (
<button onClick={this.handleClick}>
Click Me
</button>
);
}
}
export default ButtonComponent;
在这个例子中,我们在构造函数中调用了
.bind(this)
来确保this
在handleClick
方法内部指向正确的上下文。
- 使用Hooks(Function Components with Hooks):在函数组件中,你通常会使用Hooks(如
useCallback
)来处理事件绑定,以避免在每次渲染时都创建新的函数实例,这可能会导致不必要的性能问题:
import React, { useCallback } from 'react';
function ButtonComponent() {
const handleClick = useCallback(() => {
alert('Button was clicked!');
}, []); // 依赖项数组,因为这里不需要依赖任何props或state,所以为空数组
return (
<button onClick={handleClick}>
Click Me
</button>
);
}
export default ButtonComponent;
注意,
useCallback
的第二个参数是一个依赖项数组,它告诉React只有当这些依赖项发生变化时,才需要重新创建回调函数。在这个例子中,我们不需要任何依赖项,所以传递了一个空数组。
- 箭头函数 :在React组件的JSX中,可以直接使用箭头函数来绑定事件处理函数。但是,这种方式会在每次渲染时都创建一个新的函数实例,这可能会导致性能问题,特别是当在列表中渲染多个元素时。
function MyComponent() {
const handleClick = () => {
console.log('Button clicked!');
};
return (
<button onClick={handleClick}>Click Me</button>
);
}
B站评论案例
-
渲染评论列表
-
删除评论实现
-
渲染导航Tab和高亮实现
-
评论列表排序功能实现
基础模版
import { useState } from 'react'
import './App.scss'
import avatar from './images/bozai.png'
/**
* 评论列表的渲染和操作
*
* 1. 根据状态渲染评论列表
* 2. 删除评论
*/
// 评论列表数据
const defaultList = [
{
// 评论id
rpid: 3,
// 用户信息
user: {
uid: '13258165',
avatar: '',
uname: '周杰伦',
},
// 评论内容
content: '哎哟,不错哦',
// 评论时间
ctime: '10-18 08:15',
like: 88,
},
{
rpid: 2,
user: {
uid: '36080105',
avatar: '',
uname: '许嵩',
},
content: '我寻你千百度 日出到迟暮',
ctime: '11-13 11:29',
like: 88,
},
{
rpid: 1,
user: {
uid: '30009257',
avatar,
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 = () => {
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">
{/* 评论项 */}
<div className="reply-item">
{/* 头像 */}
<div className="root-reply-avatar">
<div className="bili-avatar">
<img
className="bili-avatar-img"
alt=""
/>
</div>
</div>
<div className="content-wrap">
{/* 用户名 */}
<div className="user-info">
<div className="user-name">jack</div>
</div>
{/* 评论内容 */}
<div className="root-reply">
<span className="reply-content">这是一条评论回复</span>
<div className="reply-info">
{/* 评论时间 */}
<span className="reply-time">{'2023-11-11'}</span>
{/* 评论数量 */}
<span className="reply-time">点赞数:{100}</span>
<span className="delete-btn">
删除
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
)
}
export default App
.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: 76px;
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;
}
完成版本
import { useState } from 'react'
import './App.scss'
import avatar from './images/bozai.png'
import orderBy from 'lodash/orderBy'
/**
* 评论列表的渲染和操作
*
* 1. 根据状态渲染评论列表
* 2. 删除评论
*/
// 评论列表数据
const defaultList = [
{
// 评论id
rpid: 3,
// 用户信息
user: {
uid: '13258165',
avatar: '',
uname: '周杰伦',
},
// 评论内容
content: '哎哟,不错哦',
// 评论时间
ctime: '10-18 08:15',
like: 88,
},
{
rpid: 2,
user: {
uid: '36080105',
avatar: '',
uname: '许嵩',
},
content: '我寻你千百度 日出到迟暮',
ctime: '11-13 11:29',
like: 88,
},
{
rpid: 1,
user: {
uid: '30009257',
avatar,
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 = () => {
// 导航 Tab 高亮的状态
const [activeTab, setActiveTab] = useState('hot')
const [list, setList] = useState(defaultList)
// 删除评论
const onDelete = rpid => {
// 如果要删除数组中的元素,需要调用 filter 方法,并且一定要调用 setList 才能更新状态
setList(list.filter(item => item.rpid !== rpid))
}
// tab 高亮切换
const onToggle = type => {
setActiveTab(type)
let newList
if (type === 'time') {
// 按照时间降序排序
// orderBy(对谁进行排序, 按照谁来排, 顺序)
newList = orderBy(list, 'ctime', 'desc')
} else {
// 按照喜欢数量降序排序
newList = orderBy(list, 'like', 'desc')
}
setList(newList)
}
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">{list.length}</span>
</li>
<li className="nav-sort">
{/* 高亮类名: active */}
{tabs.map(item => {
return (
<div
key={item.type}
className={
item.type === activeTab ? 'nav-item active' : 'nav-item'
}
onClick={() => onToggle(item.type)}
>
{item.text}
</div>
)
})}
</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">
{/* 评论项 */}
{list.map(item => {
return (
<div key={item.rpid} className="reply-item">
{/* 头像 */}
<div className="root-reply-avatar">
<div className="bili-avatar">
<img
className="bili-avatar-img"
src={item.user.avatar}
alt=""
/>
</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.uid === item.user.uid && (
<span
className="delete-btn"
onClick={() => onDelete(item.rpid)}
>
删除
</span>
)}
</div>
</div>
</div>
</div>
)
})}
</div>
</div>
</div>
)
}
export default App