React 条件与列表渲染、组件复用和表单状态
本文目录
这篇笔记最初发布于 2018 年,原示例混用了旧式类组件、错误的 ref 拼写和不稳定列表 key。下面保留原发布日期与主题,改用现代函数组件说明条件渲染、列表、组件复用和表单状态。历史文章里的 ReactDOM.render、类组件和路由 API 不能不看版本就照搬到当前项目。
条件渲染
React 使用 JavaScript 表达条件。分支多时优先用普通 if,让加载、失败、空数据和成功状态彼此清楚;只有两种简单结果时,三元表达式通常更易读。
function ProductPanel({ products, loading, error }) {
if (loading) return <p role="status">正在加载商品…</p>;
if (error) return <p role="alert">商品暂时不可用。</p>;
if (products.length === 0) return <p>还没有商品。</p>;
return <ProductList products={products} />;
}
condition && <Element /> 适合“成立时显示、否则不显示”的短表达式。不过 JavaScript 的 && 会返回实际操作数:如果左侧是数字 0,React 会把 0 渲染出来。列表数量判断应写成 products.length > 0 && <ProductList />,或在组件前面明确处理空状态。
列表与稳定 key
map() 将数据映射成 JSX。每个直接由列表生成的兄弟节点都需要稳定且在同级唯一的 key,让 React 能在插入、删除、排序后识别同一个业务实体。
function ProductList({ products }) {
return (
<ul>
{products.map((product) => (
<li key={product.id}>
<strong>{product.name}</strong>
<span>{product.price} 元</span>
</li>
))}
</ul>
);
}
优先使用数据库 ID 或创建时保存的业务 ID。不要用可能重复的标题,也不要在渲染时生成随机 key;数组索引只适合不会插入、删除、重排且没有局部输入状态的静态列表。key 是 React 用于协调节点的提示,不会作为普通 prop 传给子组件;子组件需要 ID 时要单独传入 productId={product.id}。
如果一项要返回多个相邻节点,可以让最外层使用 <div>,或用带 key 的 Fragment:
import { Fragment } from 'react';
const rows = products.map((product) => (
<Fragment key={product.id}>
<h2>{product.name}</h2>
<p>{product.description}</p>
</Fragment>
));
组件复用与高阶组件
高阶组件(HOC)是接收组件并返回新组件的函数。它可以为一组旧组件统一包装加载状态、权限提示或埋点等横切行为;它不是 React 的特殊语法,也不应该只为抽象几行 JSX 而引入。
function withLoading(WrappedComponent) {
function WithLoading({ loading, ...props }) {
if (loading) return <p role="status">正在加载…</p>;
return <WrappedComponent {...props} />;
}
WithLoading.displayName = `withLoading(${WrappedComponent.displayName || WrappedComponent.name || 'Component'})`;
return WithLoading;
}
HOC 的边界要明确:
- 不要在组件渲染期间创建 HOC,否则组件类型每次变化,内部状态和输入焦点可能被重置。
- 注入属性可能覆盖原属性;给注入字段使用清楚的命名,并定义冲突规则。
- 包装层级过深会让调试和测试困难。新代码中的共享行为通常先评估自定义 Hook、组合组件或普通函数。
- HOC 本身不提供权限安全。关键角色和资源授权仍由服务端执行。
受控与非受控表单
受控输入由 React state 提供当前值;输入事件需要同步更新 state。它适合实时校验、联动展示和需要随输入重新计算的界面。
import { useState } from 'react';
function SearchForm() {
const [query, setQuery] = useState('');
return (
<label>
搜索商品
<input value={query} onChange={(event) => setQuery(event.target.value)} />
</label>
);
}
文本输入的受控值应始终是字符串,避免先传 undefined、后来又传字符串而在受控/非受控之间切换。复选框与单选框要用 checked / defaultChecked,其变化值从 event.target.checked 读取。
非受控输入由 DOM 保存编辑值。只需要初始值时用 defaultValue;提交时可以从表单读取 FormData,不必为了每个字段都创建 state:
function ContactForm() {
function handleSubmit(event) {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const email = formData.get('email');
// 把字段提交到经过授权和校验的应用服务。
console.log(email);
}
return (
<form onSubmit={handleSubmit}>
<label>
邮箱
<input name="email" type="email" defaultValue="" />
</label>
<button type="submit">提交</button>
</form>
);
}
受控还是非受控应按交互需要选择,而不是把其中一种当作唯一标准。无论哪种方式,客户端校验都只是体验层;服务端仍需重新验证字段类型、长度、格式、权限和业务规则。
ref:访问 DOM 或保存非渲染值
函数组件通过 useRef 保留一个可变的 current 值。把它传给 DOM 节点可在节点提交后访问该节点;修改 ref 不会触发重新渲染,因此不要用它保存应该显示在界面上的状态。
import { useRef } from 'react';
function SearchInput() {
const inputRef = useRef(null);
return (
<div>
<input ref={inputRef} aria-label="搜索词" />
<button type="button" onClick={() => inputRef.current?.focus()}>
聚焦输入框
</button>
</div>
);
}
常见用途是聚焦、测量布局、控制媒体或接入必须持有实例的第三方 DOM API。能用 state 和声明式属性表达的界面优先使用 React 数据流;访问 ref 前还要考虑节点尚未挂载或已经卸载的时机。
自查清单
- 条件是否明确区分加载、错误、空和成功?数字
0是否会被意外渲染? - 列表 key 是否稳定、同级唯一,并来自数据而不是渲染时生成?
- 受控表单值是否始终保持正确类型,
onChange是否同步更新? - 只有需要操作 DOM 时才使用 ref;展示状态是否仍由 state 驱动?
- 客户端隐藏或校验是否被误当作服务端授权?
官方资料
Live Notes
留言提交后会先进入审核,审核通过后对其他读者公开。