文章目录
在React开发中,列表渲染是一个非常常见且重要的功能。本文将通过一个实际的评论列表渲染例子,详细介绍如何使用React的
useState
钩子和数组方法动态渲染和更新列表。本文内容从基础到进阶,涵盖了核心概念和实现方法,帮助开发者掌握React中的列表渲染技术。
一、引言
在现代Web开发中,动态数据的渲染和更新是常见需求。React作为一个强大的前端框架,通过其灵活的状态管理和组件化设计,使得处理这些需求变得更加简单和高效。本文将通过一个完整的App组件代码,详细解析其中的评论列表渲染和更新的实现。
二、初始状态与状态更新
1. 使用useState
钩子管理状态
在React函数组件中,useState
钩子用于声明状态变量并提供更新函数。以下代码声明了一个初始评论列表的状态变量list
,并使用setList
函数来更新它:
const [list, setList] = useState(defaultList);
2. 评论列表的初始数据
const defaultList = [ { rpid: 3, user: { uid: '13258165', avatar: '', uname: '周杰伦', }, content: '哎哟,不错哦', ctime: '10-18 08:15', like: 88, }, // 其他评论数据... ];
三、列表渲染的实现
在React中,使用map
方法可以方便地将数组数据转换为React元素。以下代码展示了如何遍历list
数组并生成评论列表:
<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>
1. list.map(item => { ... })
1.1. map
函数
map
是 JavaScript 中数组的一个方法,用于遍历数组中的每个元素,并返回一个新的数组。map
方法接收一个回调函数作为参数,回调函数会对数组中的每个元素执行操作,然后返回新数组中的元素。具体语法如下:
const newArray = array.map(callback(element, index, array));
callback
是对每个元素执行的函数。element
是当前处理的元素。index
是当前元素的索引。array
是被遍历的数组。
在这段代码中,map
方法用于遍历 list
数组中的每个 item
,并返回包含 JSX 元素的数组。
1.2. 箭头函数 (item => { ... })
箭头函数是 ES6 中引入的一种更简洁的函数表达方式。箭头函数没有自己的 this
绑定,而是从作用域链的上一层继承 this
。它的语法如下:
const functionName = (parameter) => { ... };
在这段代码中,箭头函数 item => { ... }
用于定义一个回调函数,其中 item
是 list
数组中的每个元素。
2. return
语句
在箭头函数的回调函数内部,return
语句用于返回一个 JSX 元素,这个元素将被放入新的数组中。具体来说,return
语句返回了一个包含评论项的 div
元素。
3. JSX 语法
JSX 是 JavaScript 的一种语法扩展,用于描述 UI 结构。它看起来很像 HTML,但实际上是 React.createElement() 的语法糖。React 会将 JSX 代码转换为 JavaScript 对象,从而构建虚拟 DOM。
在这段代码中,JSX 用于描述每个评论项的结构。具体的 JSX 代码如下:
<div key={item.rpid} className="reply-item"> {/* 列表项的内容 */} </div>
key={item.rpid}
:key
属性是 React 中用于唯一标识每个列表项的标识符,有助于优化列表的渲染和更新。className="reply-item"
:className
是 JSX 中用于指定 CSS 类名的属性,相当于 HTML 中的class
。
4. 为什么这样设计
4.1. 使用 map
渲染列表
使用 map
函数渲染列表是一种常见的模式,因为它能够简洁而高效地遍历数组,并生成一组对应的 JSX 元素。每个元素都可以根据需要进行个性化渲染。
4.2. 使用 key
属性
在渲染列表时,React 需要一个唯一的 key
属性来区分不同的元素,从而高效地更新和重新渲染组件。key
属性的值应当是唯一的,且尽量稳定,以避免因重新渲染而导致的性能问题或潜在的 Bug。
4.3. 使用箭头函数
箭头函数使得代码更加简洁,同时避免了传统函数中的 this
绑定问题。在这个例子中,箭头函数用于定义 map
函数的回调,使得代码更易于阅读和维护。
5. 完整解读
{list.map(item => { return ( <div key={item.rpid} className="reply-item"> {/* 列表项的内容 */} </div> ) })}
list.map(item => { ... })
:对list
数组中的每个元素item
执行回调函数。return
:返回一个包含评论项的 JSX 元素。<div key={item.rpid} className="reply-item">...</div>
:为每个评论项生成一个div
元素,并指定唯一的key
属性和 CSS 类名。
四、列表项的唯一标识
在渲染列表时,确保每个列表项都有一个唯一的key
属性是至关重要的。这里使用评论的rpid
作为key
:
<div key={item.rpid} className="reply-item">
1. key
的作用
当 React 渲染列表时,它需要一个唯一的标识符来跟踪每个元素,以便在元素发生变化时可以高效地重新渲染和更新。这主要体现在以下几个方面:
元素的识别
key
帮助 React 识别哪些元素发生了变化、被添加或被移除。例如,当我们有一个列表并对其进行重新排序时,key
使 React 能够知道哪些项是相同的,哪些项是新增的或删除的。
提高渲染性能
有了 key
,React 可以根据变化最小化 DOM 操作。例如,如果一个列表项的位置发生了变化,而它的 key
没有变,React 只会移动 DOM 元素,而不是销毁旧元素并创建新元素,这大大提高了性能。
防止潜在的 Bug
没有 key
或 key
不唯一,可能导致一些潜在的 Bug。例如,输入框中的内容可能会丢失,组件的状态可能会出错等。key
保证了每个元素在其父元素中都是独一无二的,这样 React 就可以正确地维护和更新组件的状态。
2. key
的用法
在列表中使用 key
的典型方式如下:
<div key={item.rpid} className="reply-item"> {/* 列表项的内容 */} </div>
这里的 item.rpid
是一个唯一标识符,对于每个列表项来说都是独一无二的。
3. 可以没有 key
吗?
不能没有 key
。如果不提供 key
,React 会发出警告,告知 key
是必要的。即使不提供 key
的代码能够工作,它也会导致性能下降和潜在的 Bug。因此,始终建议在列表渲染中为每个元素提供一个唯一的 key
。
4. 示例代码中的 key
代码中,key
的用法如下:
{list.map(item => { return ( <div key={item.rpid} className="reply-item"> {/* 列表项的内容 */} </div> ) })}
这个 key={item.rpid}
确保了每个 reply-item
元素都有一个唯一标识符 rpid
,这样 React 可以高效地更新和重新渲染评论列表。
五、处理评论删除
1. 删除评论的实现
在React中,要删除列表中的某个元素,可以使用数组的filter
方法并调用setList
来更新状态。以下是删除评论的实现代码:
const onDelete = rpid => { setList(list.filter(item => item.rpid !== rpid)); };
2. 条件渲染删除按钮
只有当前登录用户的评论才会显示删除按钮。这通过条件渲染来实现:
{user.uid === item.user.uid && ( <span className="delete-btn" onClick={() => onDelete(item.rpid)} > 删除 </span> )}
- 条件渲染
{user.uid === item.user.uid && ( ... )}
这一部分使用了逻辑与运算符(&&
)来进行条件渲染。在JavaScript中,&&
运算符在第一个操作数为真时返回第二个操作数,否则返回第一个操作数。在React的JSX语法中,这种写法常用于条件渲染。
user.uid
:表示当前登录用户的ID。item.user.uid
:表示评论作者的ID。
这段代码的逻辑是:如果user.uid
等于item.user.uid
(即当前用户是评论的作者),则渲染<span>
标签及其内容,否则不渲染任何内容。
<span>
标签
<span className="delete-btn" onClick={() => onDelete(item.rpid)} > 删除 </span>
如果条件成立,这段代码将渲染一个<span>
标签,具体解析如下:
className="delete-btn"
className
:设置元素的CSS类。这里将<span>
标签的CSS类设置为"delete-btn"
,以便应用相应的样式。
onClick={() => onDelete(item.rpid)}
onClick
:React中的事件处理属性,用于处理点击事件。{() => onDelete(item.rpid)}
:这是一个箭头函数,点击时会调用onDelete
函数,并传递当前评论的ID(item.rpid
)作为参数。
当用户点击这个<span>
标签时,onDelete
函数会被调用,传递的rpid
参数用于识别需要删除的评论。
箭头函数为什么设计成这样
- 避免立即执行
如果直接写成 onClick={onDelete(item.rpid)}
,会导致在渲染时立即执行 onDelete
函数,而不是在点击时执行。这样会立即删除评论,而不是等待用户点击删除按钮。因此,需要使用一个函数包装起来,确保 onDelete(item.rpid)
只有在点击事件发生时才执行。
- 创建事件处理函数
通过定义一个箭头函数 () => onDelete(item.rpid)
,我们创建了一个新的函数。当点击事件发生时,这个函数会被调用,然后再调用 onDelete
并传递参数 item.rpid
。
- 结合完整示例
- 如果当前登录用户(
user.uid
)与评论的作者(item.user.uid
)是同一个人,则渲染一个带有"delete-btn"
类的<span>
标签,显示“删除”文本。 - 点击“删除”按钮时,调用
onDelete
函数,并传递当前评论的ID(item.rpid
)以删除该评论。
六、完整代码示例
以下是包含所有上述功能的完整代码示例:
import { useState } from 'react'; import './App.scss'; import avatar from './images/bozai.png'; import orderBy from 'lodash/orderBy'; const defaultList = [ { 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 = { uid: '30009257', avatar, uname: '黑马前端', }; const tabs = [ { type: 'hot', text: '最热' }, { type: 'time', text: '最新' }, ]; const App = () => { const [activeTab, setActiveTab] = useState('hot'); const [list, setList] = useState(defaultList); const onDelete = rpid => { setList(list.filter(item => item.rpid !== rpid)); }; const onToggle = type => { setActiveTab(type); let newList; if (type === 'time') { newList = orderBy(list, 'ctime', 'desc'); } else { newList = orderBy(list, 'like', 'desc'); } setList(newList); }; return ( <div className="app"> <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"> {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;