2.4 条件渲染与列表渲染


文档摘要

2.4 条件渲染与列表渲染 本节摘要:条件渲染与列表渲染是把数据变成界面的两种核心映射。条件渲染有 if/else、三元、逻辑与三种主流写法,各有适用场景;列表渲染靠 map 生成,key 的选择直接决定更新性能与状态正确性。本节用登录态判断、未读消息提示、动态表格三个例子把这两件事练透。 本节导读 阅读完本节,你应当能够: 用四种方式实现条件渲染,说出各自的适用场景 解释逻辑与渲染中数字 0 的陷阱 用 map 渲染列表,正确选择 key 实现一个动态表格并处理增删行的 key 问题 理解「条件 + 列表」组合渲染的常见结构 一、问题与直觉:数据怎么变成可见界面 组件拿到数据后,第一件事是决定「显示什么、显示多少」。这两个问题分别是条件渲染和列表渲染。 条件渲染:根据条件显示不同内容。

2.4 条件渲染与列表渲染

本节摘要:条件渲染与列表渲染是把数据变成界面的两种核心映射。条件渲染有 if/else、三元、逻辑与三种主流写法,各有适用场景;列表渲染靠 map 生成,key 的选择直接决定更新性能与状态正确性。本节用登录态判断、未读消息提示、动态表格三个例子把这两件事练透。

本节导读

阅读完本节,你应当能够:

  1. 用四种方式实现条件渲染,说出各自的适用场景
  2. 解释逻辑与渲染中数字 0 的陷阱
  3. 用 map 渲染列表,正确选择 key
  4. 实现一个动态表格并处理增删行的 key 问题
  5. 理解「条件 + 列表」组合渲染的常见结构

一、问题与直觉:数据怎么变成可见界面

组件拿到数据后,第一件事是决定「显示什么、显示多少」。这两个问题分别是条件渲染和列表渲染。

  • 条件渲染:根据条件显示不同内容。登录了就显示「欢迎回来」,没登录显示「请登录」。
  • 列表渲染:把数组变成一组界面。一个 todo 数组变成一组列表项。

听起来简单,但两者都是 JSX 表达式世界里的事——而 JSX 里不能直接写 if 语句,得靠表达式变通。本节的核心就是这些变通写法以及它们各自的坑。

二、核心原理:条件渲染的四种写法

写法一:if/else 语句(组件函数体内)

最直白的写法,在 return 之前用 if 决定返回什么:

function Greeting(props) { const isLoggedIn = props.isLoggedIn; if (isLoggedIn) { return <h1>欢迎回来!</h1>; } return <h1>请先登录。</h1>; }

适用场景:分支逻辑复杂、每个分支要返回一大段 JSX 时。它只能在函数体内用,不能写进 JSX 表达式。

写法二:三元运算符(JSX 内联)

在 JSX 表达式里根据条件二选一:

function Greeting({ isLoggedIn }) { return ( <div> {isLoggedIn ? <h1>欢迎回来!</h1> : <h1>请先登录。</h1>} </div> ); }

适用场景:两个分支都比较短、需要内联在 JSX 里。三元可以嵌套,但嵌套超过两层可读性就崩了——这时候应该拆成写法一或抽小组件。

写法三:逻辑与 &&(只有条件成立才显示)

只想「条件成立时显示某内容,不成立时什么都不显示」:

function Mailbox({ unreadMessages }) { return ( <div> <h1>你好!</h1> {unreadMessages.length > 0 && ( <h2>你有 {unreadMessages.length} 条未读消息。</h2> )} </div> ); }

原理:JSX 里 条件 && 元素,条件为真返回元素,条件为假返回 false——而 React 渲染 false 时什么都不显示。

这里有个经典陷阱:如果条件不是布尔值,而是数字 0,React 会渲染出「0」这个字符。看这个翻车案例:

// 列表为空时,页面上会出现一个 "0" {items.length && <p>共 {items.length} 项</p>}

当 items.length 为 0 时,0 && <p> 的结果是 0,React 渲染数字 0。正确的写法是把条件写成布尔比较:

{items.length > 0 && <p>共 {items.length} 项</p>}

⚠️ 常见坑length && 在数组为空时渲染出 0。任何非布尔值的 && 左操作数都要小心——0、空字符串、NaN 都是假值,但它们会被 React 原样渲染出来。统一用比较表达式把条件变成布尔值,最省心。

写法四:变量赋 JSX(提前算好)

先把 JSX 算好存进变量,再在 return 里渲染:

function Greeting({ isLoggedIn }) { let message; if (isLoggedIn) { message = <h1>欢迎回来!</h1>; } else { message = <h1>请先登录。</h1>; } return <div>{message}</div>; }

适用场景:分支多、每个分支的 JSX 较长,用变量把「计算逻辑」和「渲染结构」分开。

条件渲染的边界情况

几个容易忽略的场景值得单列。

三元里渲染列表:条件决定渲染哪组列表时,把 map 写进三元的分支:

{isAdmin ? <ul>{adminItems.map(i => <li key={i.id}>{i.name}</li>)}</ul> : <p>无权查看</p>}

返回 null 隐藏组件:条件不满足时,除了逻辑与,还可以直接返回 null——React 渲染 null 不产生任何节点。适合「组件内部判断自己要不要显示」的场景:

function Promotion({ active }) { if (!active) return null; return <div className="banner">限时优惠</div>; }

多重条件用对象映射:条件多于两个且都是等值判断时,对象映射比一长串三元更清晰:

const STATUS_TEXT = { pending: '待处理', processing: '处理中', done: '已完成', failed: '失败', }; function StatusLabel({ status }) { return <span>{STATUS_TEXT[status] ?? '未知状态'}</span>; }

这类写法在状态机场景(订单状态、流程状态)里很常用,比嵌套三元可读性强得多。

四种写法选型对比

写法 位置 适合场景 注意
if/else 函数体内 分支复杂、JSX 长 不能内联进 JSX
三元 JSX 内联 两个短分支 别嵌套太深
逻辑与 JSX 内联 只显示/不显示 警惕数字 0
变量赋值 函数体内 分支多、JSX 长 与 if 配合

三、核心原理:列表渲染与 key

map 生成列表

把数组变成一组 JSX 元素,用 map:

function NumberList({ numbers }) { return ( <ul> {numbers.map((number) => ( <li key={number.toString()}>{number}</li> ))} </ul> ); }

map 遍历数组,每个元素返回一个 JSX,最终返回一个新数组。React 会把数组里的元素按顺序渲染出来。

map 之外:其他数组方法的运用

map 是列表渲染的主力,但不是唯一。不同场景配合不同的数组方法,能让渲染代码更贴合意图:

  • filter:先过滤再渲染,如「只显示未完成的 todo」
  • sort:排序后再渲染,注意排序会生成新数组,别改原数组
  • slice:截取一段渲染,如「只显示前十条」
  • reduce:列表数据的聚合,如「统计总价」——但这通常应该在渲染外算好再传入

一个实用的组合模式是「链式处理」——filter 完再 map:

function ActiveTodos({ todos }) { const active = todos.filter(t => !t.done); return ( <ul> {active.map(t => <li key={t.id}>{t.text}</li>)} </ul> ); }

先过滤得到目标子集,再 map 成界面。逻辑清晰,也避免了在 map 回调里写一堆条件判断。

数组内嵌的列表处理

另一种常见数据形态:数组里嵌套对象,对象里还有数组。比如评论列表,每条评论下有回复列表:

function CommentList({ comments }) { return ( <ul> {comments.map(c => ( <li key={c.id}> <p>{c.author}:{c.text}</p> {c.replies.length > 0 && ( <ul> {c.replies.map(r => ( <li key={r.id}>{r.author}:{r.text}</li> ))} </ul> )} </li> ))} </ul> ); }

嵌套列表的关键和前面一样:每一层 map 都要有正确的 key,且里外层 key 各自独立。内层 replies 的 key 只需在内层兄弟间唯一,不必和上层区分——这印证了第 2.1 节「key 只需要兄弟间唯一」的说法。

key:为什么必须给

第 2.1 节 Diff 讲过 key 的作用:让 React 识别「同一个元素」,在列表增删移动时复用节点、保持状态。这里从列表渲染的视角再强调一次选择原则:

key 来源 是否推荐 原因
数据自带 id 推荐 唯一且稳定
索引 index 避免 增删后索引漂移,状态错位
内容本身 仅内容不变时 内容变化等于 key 变

用 index 当 key 的具体危害用一个例子说清。一个可增删的输入框列表,用户在第一项里输入了内容,然后在中间插入一项。index 全部右移,React 按 key 复用节点,输入框的 DOM 没动,但 state 对应关系已经错位——你在第一项输入的内容,可能出现在别的项上。

key 放在哪

key 加在 map 返回的最外层元素上:

// 正确:key 在 li 上 {items.map(item => <li key={item.id}>{item.name}</li>)} // 错误:key 放在了子元素上,React 没识别到 {items.map(item => <li><span key={item.id}>{item.name}</span></li>)}

key 是 React 内部机制,不会传到组件的 props 里。也不要在组件内部尝试读 props.key——它是 React 自己用的。

动态表格:列表渲染的完整实践

SOURCE 原文给了一个动态表格案例,把它完整展开。需求:展示数据表格,数据来自数组,每行有删除按钮。

function DataTable({ rows, onDelete }) { return ( <table> <thead> <tr><th>ID</th><th>名称</th><th>操作</th></tr> </thead> <tbody> {rows.map(row => ( <tr key={row.id}> <td>{row.id}</td> <td>{row.name}</td> <td> <button onClick={() => onDelete(row.id)}>删除</button> </td> </tr> ))} </tbody> </table> ); }

这个例子把本节知识全用上了:map 生成行、key 用数据 id、每行的按钮 onClick 用箭头函数捕获 row.id。删除某一行后,数据数组少一项,React 依据 key 精确移除对应行,其他行不动——你可以把它扩展成带排序、带筛选的完整表格组件,结构和这里完全一致。

💡 关键直觉:key 是 React 世界里元素的「身份证明」。身份稳定,React 就知道谁是谁,可以精准更新;身份漂移,React 就会认错人。列表、表格、卡片网格——凡是用 map 生成的一组元素,都要先想清楚「什么能唯一标识这一项」。

四、条件与列表的组合:常见结构

真实界面里两者经常一起出现。三种高频组合:

空态处理——列表为空时显示占位:

function TodoList({ todos }) { if (todos.length === 0) { return <p>暂无待办,休息一下。</p>; } return ( <ul> {todos.map(todo => <li key={todo.id}>{todo.text}</li>)} </ul> ); }

加载/错误/内容三分支——请求数据的组件标准结构:

function UserProfile({ isLoading, error, user }) { if (isLoading) return <p>加载中...</p>; if (error) return <p>出错了:{error.message}</p>; return ( <div> <h1>{user.name}</h1> <p>{user.email}</p> </div> ); }

过滤后的列表——先算后渲染:

function FilteredList({ items, keyword }) { const filtered = items.filter(item => item.name.includes(keyword) ); return ( <ul> {filtered.map(item => <li key={item.id}>{item.name}</li>)} </ul> ); }

这三种结构几乎覆盖了真实项目里「列表 + 条件」的全部场景。它们的共同点是:先把数据算好(过滤、判断),再在 JSX 里渲染——保持 JSX 结构简单,把逻辑留在外面。

列表渲染的性能意识

列表渲染还有一个性能话题提前点一下:当列表很长(上千项)、每项的渲染成本又不低时,一次性渲染全部会导致首屏卡顿。第 3.1 节性能优化里会讲虚拟滚动——只渲染可见区域内的项。这里先建立意识:map 生成列表简单,但列表规模上去后,渲染策略要升级。一般的经验线是:几百项以内 map 没问题,几千项以上要考虑虚拟化,同时配合 key 和记忆化控制重复渲染。

key 与组件状态的关系再强调

用一个小实验体会 key 的状态关联:给一组带输入框的列表项加不加 key、用什么 key,行为差异一目了然。列表项内部有输入框,用户在第一个输入框打字。此时在数组头部插入新项:

  • key 用 id:React 识别出第一个是新增,原来的项带着输入框原样下移,输入内容还在。
  • key 用 index:所有 index 右移,React 认为第一个元素被替换成新元素,原来的输入框被重建,输入内容丢失。

这就是「index 当 key 导致输入框内容串位」的完整机理。它不总是出问题,但一旦出,极其难排查——因为表现是「输入框内容莫名消失或错位」,看起来像灵异事件。

五、工程实践:状态与渲染的边界

最后提醒一个容易混淆的点:条件渲染和列表渲染里用到的数据,应该来自 props 或 state,不要每次渲染时现场计算再存到 state。比如「过滤后的列表」这种派生数据,直接在渲染时用 filter 算即可,不要专门 setState 存一份——它可以从原始数据推导出来,存一份反而容易和原始数据不同步。

这个原则叫「派生状态勿存」:能从已有数据算出来的,就别单独存。它省去了同步的烦恼,也避免了一类隐蔽 bug——原始数据变了,派生副本忘了更新。坚持这一条,你的 state 里只会剩下「真正需要用户交互修改的数据」,其余都是渲染时算出来的临时结果,代码的可靠性会明显上一个台阶。

常见疑问快答

「条件渲染在 JSX 里写还是抽出来写?」 没有绝对答案,但有判断标准:分支少、JSX 短,写在 JSX 里(三元/逻辑与)读起来连贯;分支多、每个分支的 JSX 长,抽出来用变量赋值或拆小组件,可读性更好。还有一个更根本的问题:条件复杂到「连组件结构都不同」时,与其在一个组件里堆条件,不如拆成两个小组件让父组件按条件渲染哪个——这是第 3.5 节组件设计模式的前置思想。原则是「JSX 里的条件越多越难读,把读代码的人当『看一眼就要懂』来对待」。

「key 用 index 什么时候是安全的?」 有一个明确的安全场景:列表「只追加、不增删中间项、不排序」。比如日志流、聊天记录里的消息列表,新消息永远追加在尾部,已有项的 index 不会漂移,index 当 key 不会出错。但「现在安全」不等于「以后安全」——一旦加了删除、插到中间、排序功能,index 立刻开始错位。所以工程上更稳的约定是「一律用稳定 id」,避免将来改功能时踩坑。唯一用 index 也放心的理由,是「这个列表的每一项都无状态、无输入框、无展开态」——状态错位无从发生,性能上也不会有复用问题。

「列表项里再套列表,key 怎么处理?」 每层 map 各自的 key 只要求「兄弟间唯一」——内层列表的 key 只需要在内层兄弟之间唯一,不必和上层区分。所以嵌套列表不用操心「key 会不会全局重复」,React 只在「同一层兄弟」之间用 key 识别元素。但要注意:内层列表如果还有增删操作,它的 key 同样要用稳定 id,不能因为「嵌套的层级深」就偷懒用 index。这条规则的底层逻辑还是第 2.1 节说的——key 是「同一层内识别同一元素」的身份证。

「条件 + 列表组合时,先过滤再渲染还是渲染时过滤?」 先过滤再渲染——在 return 之前把「要显示的数组」算出来,JSX 里只做纯渲染。好处有三个:一是 JSX 结构简单,只有一层 map,没有嵌套三元套 map 的复杂结构;二是过滤逻辑可以单独测试、单独复用;三是性能上只遍历一次。反面写法是「在 JSX 里嵌套 map + 条件」,读起来像迷宫,还容易在每次渲染时重复计算。第 2.4 节的 FilteredList 就是标准姿势:先 const filtered = items.filter(...),再 map 渲染。

温故知新

  • 四种条件渲染:if/else、三元、逻辑与、变量赋值,按分支复杂度选择
  • 数字 0 陷阱:非布尔值做 && 左操作数,假值可能被原样渲染
  • 返回 null:组件内部判断不显示时直接返回 null,不产生节点
  • 对象映射:多重等值判断用对象查表,比嵌套三元清晰
  • map 列表:数组 → JSX 元素数组,React 顺序渲染
  • 链式处理:filter 再 map,先取子集再渲染
  • key 三原则:唯一、稳定、用数据 id 而非 index
  • key 位置:加在 map 返回的最外层元素上
  • 嵌套列表:每层 map 各自有 key,只需兄弟间唯一
  • 动态表格:map + key + 箭头函数捕获行数据
  • 组合结构:空态、加载/错误/内容三分支、过滤列表
  • 派生状态勿存:能从原始数据算出的,不单独 setState
  • 渲染顺序:先算数据再渲染,JSX 里少写逻辑

下一节看跨层状态共享——Context。组件树一深,props 层层传变得繁琐,Context 提供了一条「绕开中间层」的通道,但也带来了订阅与性能的取舍——值一变,所有订阅组件一起重渲染,怎么用好它是有讲究的。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U