Skip to content

JSX、组件与 props:把商品数据翻译成界面 ​

问题:三个卡片应该复制三份标签吗 ​

商品卡都有标题、价格和库存提示。如果复制三份 JSX,字段一变就要改三处。更可靠的办法是:父组件拥有数据,ProductCard 接收一件商品并描述它现在应该长什么样。

完整实验:替换 src/App.jsx ​

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 只先当作列表标识使用,本章列表篇会专门观察它。

从调用追踪到结果 ​

以乌龙茶为例:

  1. App 读取 products,map 为每个对象创建一个 ProductCard 元素;
  2. React 调用 ProductCard,参数对象里有 product 与 children;
  3. ProductCard 根据 stock 计算普通字符串 stockText;
  4. JSX 中的大括号把 JavaScript 值嵌入描述;<Price> 会继续调用 Price;
  5. React 把这一轮组件返回的元素树提交到 DOM。

JSX 不是字符串,也不是浏览器直接执行的 HTML。构建工具会把它转换成创建 React 元素的 JavaScript。因而它遵循几条语法规则:返回一个根节点;标签必须闭合;DOM 属性通常用 JavaScript 风格名称,如 className、htmlFor。

组件名必须以大写字母开头。<Price> 表示调用自定义组件;<price> 会被理解为名为 price 的原生标签。组件应在模块顶层定义,不要在 App 每次运行时重新定义一个新的组件类型。

大括号里是表达式,表达式的结果还要能显示 ​

{product.name} 读取字符串,{product.stock > 0 ? '有货' : '缺货'} 选择一个字符串。大括号不是一段任意脚本的容器,不能直接塞入 if 声明;可以在 return 前用 if 计算值,再把值放进 JSX。

把下面两行临时放到 ProductCard 的标题后,比较缺货商品的显示:

jsx
<>
  <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。

下一篇 ​

事件与受控表单

用心记录每一步成长