JSX、组件与 props:把商品数据翻译成界面
问题:三个卡片应该复制三份标签吗
商品卡都有标题、价格和库存提示。如果复制三份 JSX,字段一变就要改三处。更可靠的办法是:父组件拥有数据,ProductCard 接收一件商品并描述它现在应该长什么样。
完整实验:替换 src/App.jsx
const products = [
{ id: 'tea', name: '乌龙茶', price: 28, stock: 4 },
{ id: 'cup', name: '玻璃杯', price: 39, stock: 0 },
]
function Price({ amount }) {
return <strong>¥{amount.toFixed(2)}</strong>
}
function ProductCard({ product, children }) {
const stockText = product.stock > 0 ? `余 ${product.stock} 件` : '暂时缺货'
return (
<article>
<h2>{product.name}</h2>
<p><Price amount={product.price} /> · {stockText}</p>
<div>{children}</div>
</article>
)
}
export default function App() {
return (
<main>
<h1>今日商品</h1>
{products.map(product => (
<ProductCard key={product.id} product={product}>
<small>商品编号:{product.id}</small>
</ProductCard>
))}
</main>
)
}在 Vite 项目运行 npm run dev。页面应显示两张卡片;价格固定为两位小数,玻璃杯显示“暂时缺货”。这里的 key 只先当作列表标识使用,本章列表篇会专门观察它。
从调用追踪到结果
以乌龙茶为例:
App读取products,map为每个对象创建一个ProductCard元素;- React 调用
ProductCard,参数对象里有product与children; ProductCard根据stock计算普通字符串stockText;- JSX 中的大括号把 JavaScript 值嵌入描述;
<Price>会继续调用Price; - React 把这一轮组件返回的元素树提交到 DOM。
JSX 不是字符串,也不是浏览器直接执行的 HTML。构建工具会把它转换成创建 React 元素的 JavaScript。因而它遵循几条语法规则:返回一个根节点;标签必须闭合;DOM 属性通常用 JavaScript 风格名称,如 className、htmlFor。
组件名必须以大写字母开头。<Price> 表示调用自定义组件;<price> 会被理解为名为 price 的原生标签。组件应在模块顶层定义,不要在 App 每次运行时重新定义一个新的组件类型。
大括号里是表达式,表达式的结果还要能显示
{product.name} 读取字符串,{product.stock > 0 ? '有货' : '缺货'} 选择一个字符串。大括号不是一段任意脚本的容器,不能直接塞入 if 声明;可以在 return 前用 if 计算值,再把值放进 JSX。
把下面两行临时放到 ProductCard 的标题后,比较缺货商品的显示:
<>
<p>直接使用数量:{product.stock && <span>有库存</span>}</p>
<p>先得到布尔值:{product.stock > 0 && <span>有库存</span>}</p>
</>第一行会显示 0,第二行不会显示提示。原因分两步:JavaScript 的 && 返回操作数,左侧为 0 就返回 0;React 会显示数字,却不显示 false、null、undefined。所以条件渲染先写出真正要判断的布尔条件。三元表达式适合两个分支,条件 && 内容 适合只在条件成立时出现的内容。
再试 {product}:普通对象不能直接作为可显示子节点,React 会报错。传给 <ProductCard product={product}> 却没有问题,因为 prop 可以携带对象,由组件决定读取哪些字段。用于调试时可写 <pre>{JSON.stringify(product, null, 2)}</pre>,明确把对象转换成字符串。
多个相邻标签可以用 <></> 包起来。这个 Fragment 不增加额外 DOM 容器;它解决 JSX 的单根表达式要求,与是否需要布局用的 <section> 是两件事。
props 是这一轮渲染的只读输入
product 由父组件拥有。子组件可以读取它,却不应执行 product.stock -= 1。那会偷偷修改父组件持有的对象,使同一对象的其他使用者也看到变化,而且没有通过 React 明确提出下一次渲染。
children 也是 prop。父组件把卡片内部的补充内容放在开始和结束标签之间,卡片决定它出现在哪个插槽。它适合表达“容器允许调用者填一块内容”,并不意味着卡片拥有这段数据。
改一个条件:把缺省值放在哪里
把 Price 改为 function Price({ amount = 0 }),然后暂时删掉一件商品的 price。缺少 prop 时会显示 ¥0.00;但若显式传入 price: null,默认参数不会接管,null.toFixed 会报错。
这说明默认值只处理 undefined,不能代替数据校验。业务上“价格缺失”和“价格为零”含义不同,就应该在数据进入组件前区分,而不是用一个视觉默认值掩盖。
边界
- props 可以是对象、函数或 JSX;“只读”约束的是组件不修改收到的输入,不代表 JavaScript 会自动冻结对象。
- 在渲染期间可以做纯计算,但不应发请求、改 DOM 或写全局变量;同一输入应能得到同样的 JSX 描述。
- 展开
<Card {...product} />适合转发字段稳定的小对象;字段多且来源混杂时,显式 props 更容易看出组件契约。 - 拆组件不是越细越好。只有当一块 UI 有独立职责、重复出现或值得单独命名时,拆分才降低认知成本。
练习:会员价格牌
实现一个完整 App.jsx,其中有 MemberPrice 组件。
契约:
MemberPrice接收price(非负数字)和可选discount(0到1,默认0);- 显示原价与折后价,均保留两位小数;
discount === 0时只显示原价; - 通过
children接收一段会员说明,并由组件放在价格后; - 不修改传入的商品对象;至少渲染一件无折扣和一件八折商品。
完成后对照答案 01。