陌上人如玉
公子世无双

1.7 条件渲染与列表渲染

1.7 条件渲染与列表渲染

一句话:条件渲染靠「表达式」(三元、逻辑与、提前 return、映射表),列表渲染靠 map + 稳定唯一的 key;要小心 && 遇到 0 会渲染出 0 的陷阱。

一、条件渲染的五种写法

// ① 三元表达式:二选一
function Status({ isOnline }) {
  return <span>{isOnline ? '在线' : '离线'}</span>;
}

// ② 逻辑与:有或没有
function Notice({ message }) {
  return <div>{message && <Alert>{message}</Alert>}</div>;
}

// ③ 提前 return(组件级条件,可读性最好)⭐
function UserProfile({ user, loading, error }) {
  if (loading) return <Skeleton />;
  if (error) return <ErrorView error={error} />;
  if (!user) return <EmptyState />;

  return <ProfileContent user={user} />;
}

// ④ 变量提取(多分支场景)
function Content({ status, data }) {
  let body;
  if (status === 'loading') body = <Skeleton />;
  else if (status === 'error') body = <ErrorView />;
  else if (!data.length) body = <EmptyState />;
  else body = <List data={data} />;

  return <div className="content">{body}</div>;
}

// ⑤ 映射表(分支多且想写得「声明式」)
const statusViews = {
  idle: <Idle />,
  loading: <Skeleton />,
  error: <ErrorView />,
  success: <Content />,
};
return <div>{statusViews[status]}</div>;

对比:

写法 适用 注意
三元 二选一、行内小片段 嵌套三层以上很难读
&& 有或没有 左侧必须是布尔值(见下节)
提前 return 组件级的多分支 逻辑清晰,推荐
变量提取 分支多且需要放在同一容器里 稍啰嗦但清晰
映射表 状态机式的场景 需要保证 status 覆盖全

二、&& 的陷阱(必考)

// ❌ count 为 0 时渲染出 0
{count && <List count={count} />}

// ❌ 同样的问题:数组长度、字符串长度、NaN
{list.length && <List />}          // 空数组会渲染 0
{name.length && <span>{name}</span>}  // 空字符串会渲染 0(len=0)
{NaN && <X />}                     // 渲染 NaN

// ✅ 四种修复
{count > 0 && <List count={count} />}
{!!count && <List count={count} />}
{Boolean(count) && <List count={count} />}
{count ? <List count={count} /> : null}

为什么:&& 的语义是「左侧为真则返回右侧,否则返回左侧」。当左侧是 0 时,0 && X 的结果是 0,而 React 会渲染数字 0(0 是「假值但可渲染」的值)。而 false / null / undefined / true 会被 React 忽略,所以它们才是「安全」的假值。

表达式的值 React 渲染结果
false、null、undefined、true 什么都不渲染 ✅
0 渲染出 0 ⚠️
NaN 渲染出 NaN ⚠️
'' 渲染空文本(不可见,但 DOM 里有个空文本节点)
[] 什么都不渲染 ✅

最佳实践:JSX 里出现 && 时,养成左边写比较表达式的习惯(list.length > 0 && ...),而不是直接写变量。

三、三元表达式的可读性

// ❌ 嵌套三元很难读
{isLoading ? <Loading /> : error ? <Error /> : data.length ? <List /> : <Empty />}

// ✅ 用提前 return 拆开
function View({ isLoading, error, data }) {
  if (isLoading) return <Loading />;
  if (error) return <Error />;
  if (!data.length) return <Empty />;
  return <List data={data} />;
}

// ✅ 或提取成变量
const content = isLoading
  ? <Loading />
  : error
    ? <Error />
    : <List data={data} />;

四、列表渲染

// 基本写法
function List({ items }) {
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>{item.name}</li>       // ⭐ key 在最外层元素
      ))}
    </ul>
  );
}

key 的三个规则:

① 必须加在 map 回调返回的【最外层元素】上
② 必须【稳定且唯一】(同一个列表内不重复、跨渲染不变)
③ 用数据的唯一 ID(数据库主键、UUID),不要用 index
// ❌ key 加错位置
{items.map((item) => (
  <div>                                  {/* 没有 key */}
    <Item key={item.id} item={item} />   {/* ⚠️ 加在了内层,无效 */}
  </div>
))}

// ✅ 正确
{items.map((item) => (
  <div key={item.id}>
    <Item item={item} />
  </div>
))}

// ✅ 需要包多个元素时用 Fragment + key
{items.map((item) => (
  <Fragment key={item.id}>
    <dt>{item.term}</dt>
    <dd>{item.desc}</dd>
  </Fragment>
))}

用 index 作 key 的问题(详见 4.5):

// ❌ 列表会在头部插入/删除/排序时出 bug
{items.map((item, index) => <Row key={index} item={item} />)}

// 具体后果(以「删除第一项」为例):
// 原列表 [A, B, C] → key = [0, 1, 2]
// 删掉 A 后 [B, C] → key = [0, 1]
// React 认为「key 0 和 1 还在,只是内容变了」→ 复用原有的 DOM 与状态
// → 如果 Row 里有输入框、展开状态、动画,就会出现「内容错位」或「状态残留」
场景 用 index 的后果
列表只读不变 没问题(可接受)
头部增删 后续所有项的内容错位、状态残留
排序 状态跟着位置走而不是跟着数据走
有内部状态的项(输入框、勾选、展开态) 状态与数据错误对应
// 确实没有唯一 ID 时的三种处理
// ① 生成稳定 ID(数据入库时分配)
// ② 用「内容 + 索引」拼一个相对稳定的 key(仍有风险)
key={`${item.name}-${item.type}`}
// ③ 列表确实只读且永不重排 → 可以用 index(但要在注释里说明原因)

五、空列表与边界状态

function ItemList({ items, loading, error }) {
  // ① 加载中
  if (loading) return <Skeleton count={3} />;

  // ② 错误
  if (error) return <ErrorState message={error} onRetry={refetch} />;

  // ③ 空数据(容易被忽略)
  if (!items.length) return <EmptyState text="暂无数据" />;

  // ④ 正常渲染
  return (
    <ul>
      {items.map((it) => <li key={it.id}>{it.name}</li>)}
    </ul>
  );
}
// 搜不到结果的空状态(与「没有数据」不同)
{keyword && !results.length && (
  没有找到与「{keyword}」相关的结果
)}

四种状态要完整:loading / error / empty / success。实际项目里「空状态」最常被遗漏,导致用户看到一片空白以为页面坏了。

六、列表渲染的进阶技巧

// ① 渲染对象(不能直接 map)
{Object.entries(userMap).map(([id, user]) => (
  <UserCard key={id} user={user} />
))}

// ② 渲染指定数量的占位元素
{Array.from({ length: 5 }, (_, i) => <Skeleton key={i} />)}

// ③ 条件过滤后再渲染
{items
  .filter((it) => it.visible)
  .map((it) => <Row key={it.id} item={it} />)}

// ⚠️ 但过滤/排序最好用 useMemo(避免每次渲染都重算)
const visible = useMemo(
  () => items.filter((it) => it.visible).sort((a, b) => a.order - b.order),
  [items]
);
{visible.map((it) => <Row key={it.id} item={it} />)}

// ④ 在列表项上复用稳定的事件处理函数(配合 memo)
const handleDelete = useCallback((id: string) => {
  setItems((prev) => prev.filter((it) => it.id !== id));
}, []);

// ✅ 用 data-* 传参 → 处理函数引用稳定 → 子组件可以 memo
<ul onClick={(e) => {
  const id = (e.target as HTMLElement).closest('[data-id]')?.getAttribute('data-id');
  if (id) handleDelete(id);
}}>
  {items.map((it) => (
    <li key={it.id} data-id={it.id}>
      {it.name}<button>删除</button>
    </li>
  ))}
</ul>
// ⑤ 长列表的性能:虚拟滚动(见 5.5 与 11.6)
// 渲染 10000 个 DOM 节点会很慢,只渲染可视区域内的元素

// ⑥ 大数据量的列表用「分段渲染」
const [visibleCount, setVisibleCount] = useState(50);
const visible = items.slice(0, visibleCount);
// 滚动到底部时 setVisibleCount((c) => c + 50)

七、常见坑

// ① && 遇到 0 / NaN
{count && <X />}                  // ❌
{list.length && <X />}            // ❌

// ② key 缺失或写错位置
{items.map((it) => <Row item={it} />)}            // ⚠️ 控制台警告
{items.map((it) => <div><Row key={it.id} /></div>)}  // ⚠️ 位置错误

// ③ 用 index 作 key
{items.map((it, i) => <Row key={i} />)}           // ⚠️ 列表变动时出 bug

// ④ 在 map 里用 {} 忘了 return
{items.map((it) => { <Row item={it} /> })}        // ❌ 返回 undefined
{items.map((it) => { return <Row item={it} />; })}// ✅
{items.map((it) => <Row item={it} />)}            // ✅

// ⑤ 直接渲染数组里的对象
{items.map((it) => <li>{it}</li>)}                // ❌ 如果 it 是对象则报错

// ⑥ 漏掉空状态
{items.map(...)}                                  // ⚠️ 空数组时页面一片空白

// ⑦ 在渲染中做过滤/排序
{items.filter(...).sort(...).map(...)}            // ⚠️ 每次渲染都重算(用 useMemo)

// ⑧ 嵌套列表的 key 冲突
<ul>{groups.map((g) => (
  <li key={g.id}>
    {g.items.map((it) => <span key={it.id} />)}   // ✅ 只要求「同层唯一」
  </li>
))}</ul>

// ⑨ 条件渲染导致组件「卸载重建」
{isVisible && <Child />}      // isVisible 变化时 Child 会卸载/挂载 → 内部状态丢失
// ✅ 需要保留状态时用 CSS 隐藏或 v-show 等价方案
<div style={{ display: isVisible ? 'block' : 'none' }}><Child /></div>

面试延伸

  1. 「React 里怎么做条件渲染?」
    五种方式:① 三元表达式(二选一);② 逻辑与 &&(有或没有);③ 提前 return(组件级的多分支,可读性最好);④ 变量提取;⑤ 映射表(状态机式场景)。核心原因是 JSX 的 { } 里只能写表达式,所以不能用 if / for 语句。

  2. 「{count && <X />} 有什么问题?」
    当 count 为 0 时,0 && <X /> 的结果是 0,而 React 会把数字 0 渲染成文本,于是页面出现一个 0。根因是「0 是假值但可渲染的值」,而 false/null/undefined 才是「会被忽略的假值」。修复方式是让左侧变成布尔值:count > 0 && ...、!!count && ... 或改用三元。

  3. 「哪些值会被 React 忽略、哪些会被渲染?」
    会被忽略:false、true、null、undefined、空数组。
    会被渲染:字符串、数字(含 0 与 NaN)、元素、数组。
    会报错:普通对象(Objects are not valid as a React child)、函数(除非作为渲染函数传递)。
    这个规则是「&& 陷阱」与「为什么不渲染布尔值」的根源。

  4. 「key 的作用是什么?为什么不能用 index?」
    key 是 React 用来判断「新旧列表中的元素是否是同一个」的身份标识,它决定了组件是复用还是重建。用 index 时,列表头部插入/删除/排序会导致「key 对应的数据变了但 key 没变」,React 于是复用错误的 DOM 与组件状态——表现为输入框内容错位、展开状态残留、动画异常。所以只要列表会变动,就必须用稳定唯一 ID。

  5. 「渲染列表时最容易犯的错误有哪些?」
    ① key 缺失(控制台警告 + 性能问题);② key 加在 map 返回的内层元素上(无效);③ 用 index 作 key;④ map 回调用了 { } 但忘记 return;⑤ 在渲染中做 filter / sort(每次渲染都重算,应 useMemo);⑥ 漏掉空状态;⑦ 直接渲染对象(报错);⑧ 内联箭头函数导致 memo 失效。

  6. 「列表的四种状态是什么?为什么容易出错?」
    loading(加载中)、error(失败)、empty(成功但无数据)、success(有数据)。其中 empty 最容易被遗漏——开发者往往只处理了「有数据」和「加载中」,导致搜索无结果或首次进入时页面一片空白,用户以为页面坏了。除了 UI,还要给 empty 状态提供引导(如「去创建第一条」的按钮)。

  7. 「{isVisible && <Child />} 会有什么副作用?」
    当 isVisible 从 true 变 false 再变回 true 时,<Child /> 会被卸载再重新挂载,因此它的内部 state、滚动位置、输入内容、动画进度都会重置。如果需要「隐藏但保留状态」,应该改用 CSS 隐藏(display: none / visibility: hidden)或用 hidden 属性,而不是条件渲染。

  8. 「长列表怎么优化?」
    分三层:① key 用稳定 ID(让 React 能正确复用);② 行组件用 React.memo 并保证 props 引用稳定(处理函数用 useCallback 或用 data-* + 事件委托);③ 虚拟滚动——只渲染可视区域内的元素,用占位元素撑起总高度(1 万条数据能把 DOM 节点从 1 万降到几十个)。此外「分页」与「分段加载」也是很实用的手段。

一句话速记

条件渲染用表达式:三元(二选一)、&&(有或没有)、提前 return(最清晰)、变量、映射表;{count && <X/>} 在 count === 0 时会渲染出 0(左侧要写比较表达式,因为 0/NaN 会被渲染、false/null/undefined 会被忽略);列表必须用 map + 稳定唯一 key,且 key 加在最外层元素上,不要用 index;四种状态(loading/error/empty/success)要写全,empty 最常漏;条件渲染会卸载重建(需要保留状态就用 CSS 隐藏)。

赞(0) 打赏
未经允许不得转载:陌上寒 » 1.7 条件渲染与列表渲染

评论 抢沙发

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续给力更多优质内容,让我们一起创建更加美好的网络世界!

微信扫一扫

支付宝扫一扫