Skip to content

React 基础与 JSX(二) ​

本页对应 React 基础与 JSX,共 8 段视频,约 1 小时 30 分。

学习目标 ​

  • 能够使用 useState 定义组件状态,并将状态渲染到 JSX 中,通过调用状态修改函数触发组件重新渲染。
  • 能够解释并遵循“状态不可变”规则,使用展开语法、filter、map 等方式正确更新数组或对象类型状态。
  • 能够使用组件拆分 B 站评论案例结构,并正确处理 SCSS 样式依赖与本地图片导入。
  • 能够基于状态完成评论列表的 map 渲染、条件渲染、删除操作,以及喜欢/不喜欢的高亮和数量联动。
  • 能够实现导航 Tab 的动态渲染、高亮切换与评论列表排序,并使用 classnames 优化动态类名处理。

知识点 ​

先弄清楚状态解决什么问题 ​

React 是数据驱动视图的框架,但真正让页面内容发生变化的“开关”是状态。想让计数器从 10 变成 11,不是去操作 DOM,而是修改状态;状态一变,组件重新渲染,页面自然更新。

怎么想:useState 调用后返回一个数组,第一项是状态值,第二项是修改状态的函数;参数是初始值。怎么做:

jsx
import { useState } from 'react'

function App() {
  const [count, setCount] = useState(10)

  return <button onClick={() => setCount(count + 1)}>{count}</button>
}

易错点:count 是用 const 声明的,为什么还能变?因为每次重新渲染都会重新执行组件函数、重新调用 useState,拿到的是一个全新的变量,第一次是 10,第二次是 11,它们并不冲突。

修改状态的规则:状态不可变 ​

为什么需要这条规则:React 判断“要不要重新渲染”时,会比较新旧状态是不是同一个引用。直接改当前状态,引用没变,React 就拒绝渲染。

怎么想:不要直接修改当前状态,而要创建新值。怎么做:

jsx
// 错误:直接改当前状态
count += 1
// 正确:创建新值
setCount(count + 1)

// 数组:添加、删除、修改
setList([...list, '橘子'])
setList(list.filter(item => item !== '苹果'))
setList(list.map(item => item === '苹果' ? 'apple' : item))

// 对象:修改属性
setUser({ ...user, name: '李四' })

易错点:list.push('橘子') 之后再 setList(list),页面不会变,因为数组还是同一个;删除用 filter,修改用 map,添加用展开运算符,核心都是“造一个新数组/新对象”。

案例结构:组件拆分与两个工程细节 ​

B站评论案例分成三块:顶部导航 Tab、中间发表评论区、底部评论列表。先用 App 组件把结构搭起来,再用两个细节把环境跑通。

第一,使用 Sass 要手动装包,否则会报“找不到模块 sass”:

bash
npm i sass
jsx
import './App.scss'

第二,本地图片不能直接把相对路径写进 src,必须先导入:

jsx
import avatar from './images/avatar.png'

<img src={avatar} alt="用户头像" />

易错点:在线图片可以直接写 URL,本地图片必须 import,否则图片会加载失败。

评论列表渲染:用 map 把状态变成 UI ​

为什么用状态存列表:点赞、删除都会改变列表内容,只有状态变化才能驱动页面更新。

jsx
const [list, setList] = useState(defaultList)

{list.map(item => (
  <div className="comment-item" key={item.rpid}>
    <img src={item.user.avatar} alt="用户头像" />
    <span>{item.user.uname}</span>
    <p>{item.content}</p>
    <span>{item.ctime}</span>
    <span>{item.like}</span>
  </div>
))}

{list.length === 0 && <div className="empty">暂无评论</div>}

易错点:key 要用唯一值,这里用 rpid;条件渲染用逻辑与 &&,不要写成 if 语句塞在 JSX 里;评论数量直接用 list.length。

删除评论:条件渲染 + filter ​

为什么不能每项都显示删除按钮:只有自己发表的评论才能删,判断依据是当前登录用户的 uid 和评论作者的 uid 是否相同。

jsx
{item.user.uid === currentUser.uid && (
  <button onClick={() => onDelete(item.rpid)}>删除</button>
)}

function onDelete(rpid) {
  setList(list.filter(item => item.rpid !== rpid))
}

易错点:不要用 splice 直接改原数组;过滤后一定要调用 setList,状态才会更新。

喜欢和不喜欢:用 map 修改数组元素 ​

高亮由 action 控制:0 未表态、1 喜欢、2 不喜欢。点击喜欢时,action 取反,同时数量加减:

jsx
function onLike(rpid) {
  setList(list.map(item => {
    if (item.rpid !== rpid) return item
    return {
      ...item,
      action: item.action === 1 ? 0 : 1,
      like: item.action === 1 ? item.like - 1 : item.like + 1
    }
  }))
}

不喜欢的业务逻辑稍有不同:如果当前已经喜欢,数量要减一;如果本来没喜欢,数量不变:

jsx
like: item.action === 1 ? item.like - 1 : item.like

易错点:修改数组元素必须用 map,不是要改的那一项要原封不动返回;改完一定要 setList。

导航 Tab:渲染、高亮与排序 ​

Tab 的文字内容不会变,所以不必做成状态,直接拿数组渲染即可;但“谁高亮”会变,需要单独一个状态记录:

jsx
const tabs = [
  { type: 'hot', text: '最热' },
  { type: 'time', text: '最新' }
]
const [activeType, setActiveType] = useState('hot')

{tabs.map(item => (
  <span
    key={item.type}
    className={activeType === item.type ? 'active' : ''}
    onClick={() => onTabChange(item.type)}
  >{item.text}</span>
))}

排序推荐用 lodash 的 orderBy,三个参数分别是“对谁排、按谁排、升序还是降序”:

bash
npm i lodash
jsx
import orderBy from 'lodash/orderBy'

function onTabChange(type) {
  setActiveType(type)
  if (type === 'time') {
    setList(orderBy(list, 'ctime', 'desc'))
  } else {
    setList(orderBy(list, 'like', 'desc'))
  }
}

易错点:orderBy 返回新数组,必须交给 setList;升序是 'asc',降序是 'desc'。

用 classnames 优化动态类名 ​

为什么需要:当一个元素有多个动态类名时,三元表达式会层层嵌套,很难读。classnames 可以把“固定类名”和“条件类名”分开写:

bash
npm i classnames
jsx
import classNames from 'classnames'

<button
  className={classNames('btn', {
    'btn-disable': disabled,
    'btn-small': size === 'small',
    'btn-large': size === 'large'
  })}
/>

单个动态类名也可以用逻辑与:classNames('btn', disabled && 'btn-disable')。易错点:对象写法中键是类名,值是布尔值,只有为 true 才会添加该类名。

先明确:这一节只做“结构” ​

为什么需要:B站评论案例后面要接状态、列表渲染、删除、点赞等逻辑,如果一开始没有稳定的 DOM 结构,后面就没有地方挂数据和事件。
怎么想:先按页面视觉把案例拆成“评论导航区、发表评论区、评论列表区”,用固定类名把骨架搭出来;数据先写死,等结构稳定后再替换成状态。
怎么做:在 App 组件中写 JSX,保留 reply-navigation、box-normal、reply-list、reply-none 等类名,样式文件单独导入。
易错点:JSX 里写 className,不要写 class;注释用 {/* ... */};图片的 alt 要写中文,便于可读性和无障碍。

三块区域怎么拆 ​

为什么需要:结构不清楚,后面做“最新/最热排序、发表评论、删除、喜欢不喜欢”时容易把事件绑错位置。
怎么想:上部是导航 Tab,中部是当前用户发表评论,下部是评论列表;列表项内部再分头像、用户信息、评论内容、操作区。
怎么做:导航区用 ul.nav-bar,发表区用 textarea 和“发布”按钮,列表区用 reply-list 包多个 reply-item;无数据时预留 reply-none。
易错点:不要把“暂无评论”和评论列表同时写死展示;后续应根据列表长度条件渲染。列表项后续要加唯一 key。

组件入口与样式 ​

为什么需要:脚手架需要一个根组件来承载案例结构,样式也要有统一入口。
怎么想:转写稿中先建 App 组件,再在入口文件里渲染它;样式单独建 app.scss 并导入。
怎么做:创建函数组件并导出,在入口中渲染 <App />;在组件顶部 import './app.scss'。
易错点:组件名要大写;样式路径要相对当前文件;不要把样式直接散落在入口文件里。

Sass 的安装与报错调试 ​

为什么需要:案例样式较多,用 Sass 方便嵌套和维护。
怎么想:React 脚手架默认不一定能解析 .scss,需要让构建工具认识 Sass 语法。
怎么做:先停项目,执行 npm i sass,再重新启动;新建 app.scss 并在组件中导入。
易错点:转写稿里“saras/萨斯”是口误,实际是 Sass;如果没装包会报“找不到模块 sass”,装完要重启项目。

本地头像的导入与破图调试 ​

为什么需要:本地图片要经过构建工具处理,不能像写普通字符串路径那样直接使用。
怎么想:把图片当成模块先导入,再把导入结果交给 src。
怎么做:按转写稿演示,把本地头像导入为 avatar,在 <img src={avatar} alt="用户头像" /> 中使用。
易错点:直接写相对路径作为 src 可能加载失败、图片破损;在线图片才可以直接写 URL。

结构代码(保留) ​

jsx
<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">9</span>
      </li>
      <li className="nav-sort">
        {/* 高亮类名: active */}
        <div className="nav-item active">最热</div>
        <div className="nav-item">最新</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">
      {/* 评论项 */}
      <div className="reply-item">
        {/* 头像 */}
        <div className="root-reply-avatar">
          <div className="bili-avatar">
            <img
              className="bili-avatar-img"
              src="https://yjy-teach-oss.oss-cn-beijing.aliyuncs.com/reactbase/comment/zhoujielun.jpeg"
              alt="周杰伦头像"
            />
          </div>
        </div>

        <div className="content-wrap">
          {/* 用户名 */}
          <div className="user-info">
            <div className="user-name">周杰伦</div>
          </div>
          {/* 评论内容 */}
          <div className="root-reply">
            <span className="reply-content">哎呦,不错哦~</span>
            <div className="reply-info">
              {/* 评论时间 */}
              <span className="reply-time">11-13 11:29</span>
              {/* 喜欢 */}
              <span className="reply-like">
                {/* 选中类名: liked */}
                <i className="icon like-icon" />
                <span>66</span>
              </span>
              {/* 不喜欢 */}
              <span className="reply-dislike">
                {/* 选中类名: disliked */}
                <i className="icon dislike-icon" />
              </span>
              <span className="delete-btn">删除</span>
            </div>
          </div>
        </div>
      </div>
    </div>

    <div className="reply-none">暂无评论</div>
  </div>
</div>

结构完成后的衔接 ​

为什么需要:当前 9、周杰伦评论、66、删除按钮都是写死的,后面必须换成状态数据。
怎么想:导航数量接列表长度;评论项用 map 渲染;删除按钮按“是否当前用户”条件渲染;喜欢/不喜欢按 action 加高亮类名。
怎么做:先把这份结构作为模板,后续逐步把写死内容替换为 list、activeType 等状态。
易错点:不要在结构阶段提前写复杂逻辑;先保证类名、层级、图片和样式正确,再进入数据驱动视图。

样式总览:先分区,再写选择器 ​

为什么需要:B 站评论案例的结构分成评论导航、发表评论框、评论列表和暂无评论四块,样式必须先按区块组织,否则选择器会互相污染。
怎么想:外层 .app 控制整体宽度和居中,内部按 .reply-navigation、.reply-wrap、.reply-list、.reply-none 分块。
怎么做:.app { width: 80%; margin: 50px auto; };列表区用 flex 排版,评论项用 padding-left: 80px 给头像留位。
易错点:百分比宽度要和 margin: auto 配合;ul 要清掉默认 margin/padding/list-style。

评论导航:flex 一行排列,伪元素做分隔线 ​

为什么需要:标题、回复总数、最热/最新要在一行内对齐,并且排序项之间要有竖线分隔。
怎么想:.nav-bar 用 flex 垂直居中;分隔线不额外加标签,用 .nav-item::after 画 1px 竖线。
怎么做:.nav-item::after 设置 height:10px; width:1px; margin:-1px 12px; background:#9499a0;,最后一项 &:last-child::after { display:none; };hover 和 .active 改文字颜色。
易错点:忘记隐藏最后一项的 ::after 会多一条线;.active 的优先级要能覆盖默认色。

回复框:聚焦时扩展,发送按钮用伪元素叠色 ​

为什么需要:输入框默认灰底,聚焦后变白并变高;发送按钮的文字要盖在蓝色背景上。
怎么想:textarea 用 transition: .2s 做过渡,:focus 改高度、背景和边框;发送按钮用 ::after 画背景,.send-text 用 z-index:1 提到上层。
怎么做:.reply-box-textarea:focus { height:60px; background:#fff; border-color:#c9ccd0; };.reply-box-send::after { opacity:.5; },:hover::after { opacity:1; }。
易错点:textarea 要 box-sizing:border-box、resize:none;伪元素必须绝对定位,否则会撑开布局。

头像与评论列表:绝对定位对齐,底部分隔线用伪元素 ​

为什么需要:头像固定在左侧,评论内容右缩进;每条评论底部要有一条浅灰分隔线。
怎么想:.reply-item 设 position:relative 和 padding-left:80px,头像 .root-reply-avatar 绝对定位到左侧;分隔线用 .content-wrap::after。
怎么做:.bili-avatar 设 48px 圆形;.bili-avatar-img 用 top:50%; left:50%; transform:translate(-50%,-50%) 居中,并 object-fit:cover。
易错点:父级没有 position:relative 时绝对定位会跑出容器;图片不设 object-fit 可能变形。

互动区与空状态:雪碧图定位,空列表单独样式 ​

为什么需要:喜欢/不喜欢图标用背景图位置切换,删除按钮 hover 变蓝;没有评论时要显示居中提示。
怎么想:.like-icon、.dislike-icon 固定宽高,通过 background-position 切换普通/高亮;.liked、.disliked 覆盖位置;.reply-none 单独做 64px 行高居中。
怎么做:.reply-info 用 flex 对齐;.delete-btn:hover { color:#00aeec; };.reply-none { height:64px; line-height:64px; text-align:center; }。
易错点:liked 与 :hover 的 background-position 可能冲突,要注意书写顺序;空状态应由数据长度条件渲染控制显示。

工程调试:Sass、本地图片、动态类名 ​

转写稿里的“萨斯”应写作 Sass,“reite”是 React,“GSS”是 JSX,“us state”是 useState,“future”是 filter,“out buy”是 orderBy,“load dash”是 lodash,“class names”是 classnames。
为什么需要:在 React 脚手架里直接用 .scss 会报“找不到模块 sass”;本地图片直接写相对路径会加载失败;多个动态类名手写三元表达式很乱。
怎么想:SCSS 需要 sass 解析包;本地图片要像 JS 模块一样 import 后再作为 src;动态类名用 classnames 统一处理。
怎么做:npm i sass 后重启项目;import avatar from './images/avatar.png';classnames('btn', { 'btn-disable': disabled })。
易错点:装包后要重启;本地图片不能直接写相对路径;classnames 对象里键是类名、值是布尔值。

下面代码中的两张 data URI 图片原样保留;头像图片在模板中应使用中文 alt="用户头像"。

scss
.app {
  width: 80%;
  margin: 50px auto;
}

.icon {
  background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA+gAAAPoCAMAAAB6fSTWAAAA51BMVEUAAACYoKhwd3yboqni5emDjJL7+/yZoqoAodbnix8AodYAodaZoqoAodYAodaln5jnix8Aodbnix8AodaZoqoAodbnix8Aodbnix/yXY6ZoqoAodYAodYAodaZoqoAodaqryXY7yXY4AodbyXY6ZoqryXY6ZoqoAodaZoqoAodaqryXY7nix8AodYAodbnix+ZoqqZoqrnix8AodYAodbnix+Zoqr////19vfM0NcAoda/v7/l6e9MyP//u1PlL+z/s3yS0eWV3bL/bAAVFRX/AACEHPnnix+M2fn/1pbyXY4iIiIkv4BgAAAAOHRSTlMA9fUreZKu4eI+EfDtgtwP7AkexYcv2WfIsP3refnX0mcmGUPyxsScjXkXF++zoZpMMyn+Ppl8Q6/LsKoAAA3QSURBVHja7NvdbtowGIfxP7UsaEqbfkGj0bWVpqofiK0f2nZALyD3f0V7E4KsbULCjpRA9fykQDjw4SOb2BEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAG2cF4X64vzAeJc+/sDYeGDH3Q0e1MrV1x9q4eW0LNUTP2j4xPEHDS9gp70O50O1MRk9j5Tu13tZhX4+LdS5ejJvpnUlqCfzZloXsMPym99qFfrZ7Telh54vyop1Xk7VNevbqeas+KT5fD2eOR3b+FhR1/L84dJaz42SZNnPR2UnWZadKV7+Mi1rss7P1THXdB7u47iq83DP/3RsijtQpevQ78bjL/fS29CMHxTrana0vDjT5MTMviuSVb6movvO5Qe+Wr2vLvsRP6H7avW+ujxTOjaErrrw+mq+1K1hrqHWxoo3yjTS2kyRTssQeh9sEg+hO/uIZJN4CN3xLx07G7pC6G/3KaErhD65UKQyUGEfhbplaYfQlRK6Quja29CPj4W/febQn55ahn59vY+hO9VcWuhh/P6GfrxcUvq/PnHo965l6BcTRZruwNLdexnv05buYfzeLt2tc0qPkBi6qb77D31+o3ahP58o1mERQl8U/TyMc3bZjUt9GOfsshvHwzhsDt00jdf3fYZ+d9ky9KtHxcsPe99ec746NJO+veZ8dWiG7TVs9PGfzkOfr0PPb16TQn9eh57dTtoemCm0NQ7MAHH76OOVJylxH/2oNrtufQR2oa1xBBbYN/ZSy7ui8VILsF94TRUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAH3buoMVNIAzA8BxESA5ldyHkUui1p/Y6YrJ71v//g/rFmFoKaaMBdZPngTWzh+/4MqKTAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwIMqyirnqizungfWqihzryzum5c6rFVkWrUfoa0i1Unzx+Y9NMfTPKzZvv6ZnlJ02n702ih1wnzz3muUzrrt6rpOS3kbFrMrzp0PpRdj57vOh9LdvbNer/WCob+9bFJn8zJ/6eWl87Y9l16OnW/6xpvuakvnvw5naW7bbX2y3W5f0xI2UXr/MbciV33nffBVLsbNH/vO++CPtnSuxT3o/k/z2td/+JGWEIkv0vmwobf596KcsqE3ORa2dK46nNLuLsNiXpF3/F2kRUTkC3QeqnzpPBadXI2bv3Qei07Mg9CvlR6dLyDnc+ehqqou9Dxu/tJ5zB+70HOCtYf+Nd3sgUKvcqedGno/3widTxL6Lt3skW7do+/ofPKtezh17tadf4YeTp8rCP1Lup2HcR7GMSL00BfeNb5o6N/TzR7r9Vobnd/zeq2Jzr1e47rD35YM/dsujfMwB2bauE4/MNMdl7Ghs2r7+o5HcY7AOgILn4AvtcAz8DVVeAZ+eAKegp+SAgAAAAAAAAAAAAAAAAAAAH6xczctbQRxAIf/RmHDGgyiQWisCkV8gxaF0nZDTjkF+v0/T4dNrIFe6g5JnOR5srksDHP6wTCzDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlKhZdXRY3HjgPzS/Vkybd5fW/FyRxmfOr3RorS/0ZHqUEXqSxufODyRrDD1pckJPmuz5gQihQxc3g8GnwcJDdHAxPp4ct8aXUR6hsx+qp6iiNbx6jvfrP0Y/WvX1KIojdDZtthCbVbVP6+a8S+jt07q4j+IsQjvIDH2eGfpU6Dtutioi2WLoT1d5oT+eRHEWof0+yAt9Ms8LvZkKfbfNoi28/be2GXrcHmaFHmflrd2XoafSs0KfzPNCb6ZC32kfK/SHh7zQL8vbjluGnkrPC30yzwu9mQp9l62Evv2le7zc5oU+OovS/A29J3Q66BT6Vjbjhm+hx6BD6PVb6DGO0ryG3rN0Z41e406/jNBzz9FvI16qZHDX7Rz97DRGJ8n4a5RmGXrPZhzr1Gb92vjyzaYNh3fnMbwaJtFFXX+/j/qkruvTKM4itJ7jNdZq9q/YuFT5j6iiu9PrL9GPIvlghj3yXD1VkWHUfxS60Pnwbg7uIsfF529RJKHDHhA67AEXT8AecJUU7IHG5ZAAAAAAAAAAAAAAAMAfdu6etUEgDuDwNcnkUMgQshS6dmrXeOKSLdDv/3kqlxeELCVXk9T/84Aogtz0w+OUAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAmVqu8ti/ex74RWe5b8dueH43Vj0+8PdWfVsV2mrofOyG8YUOU8ttXWh5Vxd6boUOV4QOt9h2F28pHqETwxD4cBTvmxSO0Lm3/VGqUBd695HCuYT2Uhn6oTL0Xuhzth8rdx4Z+msKJ587/64L/dDVhd5noc/ZPpXCy1E8LPQi3tw9nzuvC/3Q1YXeZ6HP2pOFHm85Lp86rwv90NWF3mehz9so9CeYug+X0Rz7WgidKzN+o0cN3dSdaZ36LufHhL7tRj5TNLk9WliMY0Il69J3xap7paYpkTdNs07h5PZk4fMa09lfS/e3Djlr98MM0WyELnQC2HZfKSShQwBChwBsPAEB2EoKIljaHBIAAAAAAAAAAPhhzw5WGwSiMIzekCGbkF1Wgb5HhzIL3/+lClaCEixCCMl4zwER3H/8OgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADtX2gYlgJ617w1aAD0TOiQgdEhA6JCA0CEBoUMCQocEhA4JCB0SEDokIHRIQOiQgNBJ6nq4xlMu50t0Q+gkdbsd4ilfP+fohtB5o+FPbGTRhU4vhrkYr+CB0OnbEPfChb5O6PTtU0L36i505l4Z+vRkI4dxQqcXi9AHi75C6PRt6nu6+0ZfIXT6NmY99i30/widrg0z/qOvEjo4jBM6WHShQ0ZChwSEDgkIHRIQOiQgdEhA6JDAQ+i1tSp02Je2rLy2cjyWVqvQYUfaYsxPJUbl1KrQYTfaYszjbpx1of+yZ8c4DINAFAW3QJwpFO64/5kiMAUU6eP1jGS5oH76loEcajvGfDlnvdUAnqxc7dOuY8yPWZ/HJYBHK3WN+e9jnQMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPyNfgsgmb6LQeiQTo9Z+P2ERYeUhA4vsIXu0x2y2kOfhA75rL7HW+iQ1cx69O2vO+TVN+7RAQAAAAAAAAAAvuzZwQnAIBBE0a1u+i8pqBch15wm74FawWdFAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAvpFjgDK5zSJ0qJPZhZ81JjpUEjr8wBW6qzu0ek10oUOfTJZ1Ch1aZW/JeHWHXrn4RwcAAAAAAHjYs2MbgIEQCIKURv9VWY8dfAGOjhkJUcFGBwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8I9+FRCmb3UIHeJ0TeFzQ+iQR+iwgNBhAaHDAl/f5wsdUk3W07fQIVZf7OgAAAAPe3ZQA0AIQ1Gw7r5/Rxu6lwrgVGYSqIIXCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANyRXwHLZKpD6LBOqgvv1UPosI/Q4QEjdFd32MqJDg9I5ThT6LBVekvKqzvslcE/+sduHZ0AAIIAFHQ5918pMggH6MvuQJzgoQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAG/kEcAw2cUmdBgnowqvqSV0mEfo8IEWutcdprqh17joiz07tgEQhgEgmBoEUuQaZZDU3n8lCBUbIFl3hT3BNzaUlC2XtYUOVeU7MpurO9SVH/7oAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAL+L+YgGVBZzaUBp2xA6FNaP8zqPmEPoUFaPueyxCf1mz45NIIaBIAAqdCKBcOTAgZBDh86uhO+/n9fzTZhjJtgOloNbSKtGm322qGX3jIOsWjwrn2gFSOuMvrLHWYC0WkwXHbKrsc0+t6gFSKvv8bP3AuT139H1HAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4OXGcV3HKEBi4/4st6Z/2bODG4BhEAaArJFnoyjLeP99WnUMuHuwgQXC0NnK2vsbBfR1sqt2TgF9CToM4HSHATzjYIJnJeo16O3mdwvoS9BhhqSA7q51DgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAve3AgAAAAAADk/9oIqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqrCHhwIAAAAAAD5vzaCqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqwBwcCAAAAAED+r42gqqqqqqqqqqqqqqqqqqqqqqqqqq

### 课件图示

![课件第31页: 使用 App 组件,搭建结构(① 使用 SASS 样式 ② 使用本地图片) ](/14-react/slides/day01/slide-31.png)

![课件第34页:B站评论案例总结 使用组件搭建结构  评论列表的渲染和操作  • 组件的使用](/14-react/slides/day01/slide-34.png)

![课件第35页:B站评论案例总结 评论列表的渲染和操作  • 状态的使用 • 列表渲染(数组的](/14-react/slides/day01/slide-35.png)

![课件第27页:状态(state)是可以让页面内容发生变化的数据 组件的状态 存储状态的变量修改](/14-react/slides/day01/slide-27.png)

![课件第29页:规则:不要直接修改当前状态的值,应该创建新值 注意:如果没有遵循该规则,会导致报](/14-react/slides/day01/slide-29.png)

![课件第36页:B站评论案例总结 导航 Tab 的渲染和操作  • lodash 的 orde](/14-react/slides/day01/slide-36.png)

![课件第6页:总结 总结 1. React 是渲染用户界面(UI)的 JavaScript 库](/14-react/slides/day01/slide-06.png)

![课件第32页:1. 根据状态渲染评论列表 2. 删除评论 3. 喜欢和不喜欢 评论列表的渲染和](/14-react/slides/day01/slide-32.png)

代码示例 ​

常见错误 ​

1. 改完数组,页面不动

  • 现象:list.push(新项) 后调用 setList(list),控制台没有报错,但列表就是不增加;用 count++ 时直接报 TypeError: Assignment to constant variable。
  • 原因:const 声明的基本类型不能重新赋值;数组和对象虽然能修改内容,但引用没有变化,React 用 Object.is 对比新旧状态时判定为同一个值,于是跳过渲染。
  • 修法:一律创建新值再交给 setter,例如 setList([...list, item])、setCount(count + 1);删除用 setList(list.filter(...)),排序用 orderBy 的返回值,不要用 list.sort() 原地排序。

2. Sass 样式一引入就白屏报错

  • 现象:新建 app.scss 并 import 后,终端提示找不到 sass 模块,页面编译失败。
  • 原因:脚手架只负责把 .scss 交给解析器,解析器本身需要单独安装。
  • 修法:先 npm i sass,然后停掉 dev server 重新启动;只改样式不重启时,该错误不会自行消失。

3. 本地图片裂图

  • 现象:<img src="../../assets/avatar.png" /> 渲染出来是破图,控制台报 404。
  • 原因:打包工具不会把字符串形式的相对路径当作模块处理,产物目录里没有这个文件。
  • 修法:import avatar from './assets/avatar.png' 后再 src={avatar};网络图片则可以直接写 URL。

4. 删除按钮每一条都出现

  • 现象:明明只有自己发的评论能删,结果三条全带删除按钮。
  • 原因:条件渲染漏写,或者比较的字段取错层级(拿 item.uid 去比 user.uid,实际数据在 item.user.uid)。
  • 修法:{item.user.uid === user.uid && <span className="delete-btn">删除</span>},先用 console.log 打印两边的值确认路径。

5. 点了喜欢,高亮变了但数字没变(或反之)

  • 现象:action 更新了,like 数量还是原值。
  • 原因:map 里 return { ...item } 只复制了旧对象,新字段在 return 之后才补,或者压根没写进新对象。
  • 修法:把要改的字段一次性写在展开之后,return { ...item, action: 1, like: item.like + 1 };改完必须 setList(新数组),忘记调用 setter 一样不渲染。

6. key 用数组下标

  • 现象:删掉中间一条后,剩下项的高亮、数量错位,控制台出现 key 警告。
  • 原因:index 会随删除重排,React 复用了错误的 DOM 节点。
  • 修法:用数据自带的唯一值 key={item.rpid}。

7. 直接把对象塞进 JSX

  • 现象:<div>{user}</div> 报 Objects are not valid as a React child。
  • 原因:对象不是合法的 React 子节点。
  • 修法:渲染具体属性 {user.name} {user.age},调试时才用 JSON.stringify。

练习 ​

  1. 用 classnames 的对象语法重写评论项的类名:静态 icon like-icon + 动态 liked,以及 icon dislike-icon + 动态 disliked,要求两个按钮共四处类名都不出现三元嵌套。

  2. 给评论列表加一个「只看自己的评论」开关:勾选后只渲染 user.uid === 当前用户 uid 的项,取消勾选恢复全部。思考这个过滤结果该不该放进 useState。

  3. 给排序加第三个 Tab「最早」:按 ctime 升序。要求不改动原 list 状态,并说明为什么不能直接 list.sort()。

  4. 实现发表评论:受控 textarea + 点击发布后把新评论插到列表头部,新评论自带删除按钮。要求处理空内容的情况。

  5. 把删除改成「点击后二次确认」,确认后删除;并验证删到 0 条时「暂无评论」自动出现。

原视频对照 ​

#视频时长
1组件的状态09:39
2修改状态的规则12:53
3B站评论案例-1使用组件搭建案例结构09:51
4B站评论案例-2评论列表的渲染07:56
5B站评论案例-2评论列表的删除06:41
6B站评论案例-3评论列表的喜欢和不喜欢16:42
7B站评论案例-3导航Tab的渲染和操作18:28
8使用classnames优化类名处理08:43

小结 ​

  1. useState 每次渲染都会重新执行,第一次拿到默认值,之后拿到最新值——所谓「常量能变」的错觉,本质是函数体重跑产生了新的变量。
  2. 判断要不要重渲染看的是引用是否变化,不是内容是否变化;因此「状态不可变」是一条执行层面的硬约束,不是编码风格建议。
  3. map 渲染、filter 删除、map 改元素、orderBy/slice+sort 排序,四件事的共同点是都返回新数组,记住这条主线,就不会写出原地修改的代码。
  4. 能算出来的数据不要存状态:评论总数、是否显示「暂无评论」都由 list.length 派生,多存一份只会多一个同步 bug。
  5. 只有会变的东西才进 state:Tab 的文字数组是常量,只有「当前选中的 type」需要状态;评论数据会变,所以整个列表进 state。
  6. 条件渲染用 &&,类名条件超过一个就换 classnames 对象语法,可读性差距随条件数量线性放大。
  7. 静态资源(scss、本地图片)必须按模块导入,路径字符串在打包后不存在——这是工程约定,不是 React 的限制。
  8. 业务逻辑优先于 API:不喜欢按钮的「数量减一 / 不变」取决于此前是否点过喜欢,先写清分支再写代码,比边写边调更快。