2.4 条件渲染与列表渲染 本节摘要:条件渲染与列表渲染是把数据变成界面的两种核心映射。条件渲染有 if/else、三元、逻辑与三种主流写法,各有适用场景;列表渲染靠 map 生成,key 的选择直接决定更新性能与状态正确性。本节用登录态判断、未读消息提示、动态表格三个例子把这两件事练透。 本节导读 阅读完本节,你应当能够: 用四种方式实现条件渲染,说出各自的适用场景 解释逻辑与渲染中数字 0 的陷阱 用 map 渲染列表,正确选择 key 实现一个动态表格并处理增删行的 key 问题 理解「条件 + 列表」组合渲染的常见结构 一、问题与直觉:数据怎么变成可见界面 组件拿到数据后,第一件事是决定「显示什么、显示多少」。这两个问题分别是条件渲染和列表渲染。 条件渲染:根据条件显示不同内容。
本节摘要:条件渲染与列表渲染是把数据变成界面的两种核心映射。条件渲染有 if/else、三元、逻辑与三种主流写法,各有适用场景;列表渲染靠 map 生成,key 的选择直接决定更新性能与状态正确性。本节用登录态判断、未读消息提示、动态表格三个例子把这两件事练透。
阅读完本节,你应当能够:
组件拿到数据后,第一件事是决定「显示什么、显示多少」。这两个问题分别是条件渲染和列表渲染。
听起来简单,但两者都是 JSX 表达式世界里的事——而 JSX 里不能直接写 if 语句,得靠表达式变通。本节的核心就是这些变通写法以及它们各自的坑。
最直白的写法,在 return 之前用 if 决定返回什么:
function Greeting(props) { const isLoggedIn = props.isLoggedIn; if (isLoggedIn) { return <h1>欢迎回来!</h1>; } return <h1>请先登录。</h1>; }
适用场景:分支逻辑复杂、每个分支要返回一大段 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 算好存进变量,再在 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 配合 |
把数组变成一组 JSX 元素,用 map:
function NumberList({ numbers }) { return ( <ul> {numbers.map((number) => ( <li key={number.toString()}>{number}</li> ))} </ul> ); }
map 遍历数组,每个元素返回一个 JSX,最终返回一个新数组。React 会把数组里的元素按顺序渲染出来。
map 是列表渲染的主力,但不是唯一。不同场景配合不同的数组方法,能让渲染代码更贴合意图:
一个实用的组合模式是「链式处理」——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 只需要兄弟间唯一」的说法。
第 2.1 节 Diff 讲过 key 的作用:让 React 识别「同一个元素」,在列表增删移动时复用节点、保持状态。这里从列表渲染的视角再强调一次选择原则:
| key 来源 | 是否推荐 | 原因 |
|---|---|---|
| 数据自带 id | 推荐 | 唯一且稳定 |
| 索引 index | 避免 | 增删后索引漂移,状态错位 |
| 内容本身 | 仅内容不变时 | 内容变化等于 key 变 |
用 index 当 key 的具体危害用一个例子说清。一个可增删的输入框列表,用户在第一项里输入了内容,然后在中间插入一项。index 全部右移,React 按 key 复用节点,输入框的 DOM 没动,但 state 对应关系已经错位——你在第一项输入的内容,可能出现在别的项上。
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,行为差异一目了然。列表项内部有输入框,用户在第一个输入框打字。此时在数组头部插入新项:
这就是「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 渲染。
下一节看跨层状态共享——Context。组件树一深,props 层层传变得繁琐,Context 提供了一条「绕开中间层」的通道,但也带来了订阅与性能的取舍——值一变,所有订阅组件一起重渲染,怎么用好它是有讲究的。