0%

已完成 0/29 关

教学模式

你可以随时调整。

此设置调整教师的教学方式,无需重新生成课程。

第 3 关 含代码 约 20 分钟

用 React 思考:将设计原型拆解为组件与数据流

用 React 思考:将设计原型拆解为组件与数据流

React 最令人兴奋的地方在于,它会彻底重塑你对网页和应用的构思方式。当你拿到设计师给出的原型图和后端提供的 JSON 数据时,不再是手忙脚乱地到处写 DOM 选择器和点击监听,而是有一套清晰严密的五步思考法则。

本关我们跟随官方经典案例——构建一个可搜索并能过滤库存的商品数据表(FilterableProductTable),完整走完这五步!


准备:设计原型与 JSON 数据

假设后端给你的 JSON 数据长成这样:

[
  { "category": "Fruits", "price": "$1", "stocked": true, "name": "Apple" },
  { "category": "Fruits", "price": "$1", "stocked": true, "name": "Dragonfruit" },
  { "category": "Fruits", "price": "$2", "stocked": false, "name": "Passionfruit" },
  { "category": "Vegetables", "price": "$2", "stocked": true, "name": "Spinach" },
  { "category": "Vegetables", "price": "$4", "stocked": false, "name": "Pumpkin" },
  { "category": "Vegetables", "price": "$1", "stocked": true, "name": "Peas" }
]

设计师给出的原型图包含:

  1. 顶部一个搜索框,以及一个“仅展示有货商品”的复选框;
  2. 下方一个表格,包含两列:名称(Name)和价格(Price);
  3. 表格按品类(Fruits、Vegetables)展示分类表头;缺货的商品名称以红色高亮显示。

步骤一:将 UI 拆解为组件层级结构

第一步,拿起笔在原型图的每个模块周围画方框,并给它们命名:

  1. FilterableProductTable(最外层大容器):包含整个应用的完整视图。
  2. SearchBar(搜索控制区):接收用户的搜索关键字输入和复选框点击。
  3. ProductTable(商品表格区):根据输入过滤并展示商品列表。
  4. ProductCategoryRow(分类表头行):展示品类标题(如 “Fruits”)。
  5. ProductRow(单个商品行):展示具体商品的名称和价格。

组件层级关系树:

  • FilterableProductTable
    • SearchBar
    • ProductTable
      • ProductCategoryRow
      • ProductRow

设计原则(关注点分离):每个组件理想情况下只做一件事。当一个组件功能变得庞大时,就应该继续将其拆解为更小的子组件。


步骤二:使用 React 构建一个静态版本

有了层级结构后,先不要急着写任何交互或状态!最简单高效的办法是先根据数据模型构建一个纯展示的静态版本。

构建静态版本只需要写大量的 JSX 结构并用 Props 传递数据。绝对不要在这一步使用任何 state! 因为静态版本没有任何随时间变化的数据。

function ProductCategoryRow({ category }) {
  return (
    <tr>
      <th colSpan="2">{category}</th>
    </tr>
  );
}

function ProductRow({ product }) {
  const name = product.stocked ? product.name : (
    <span style={{ color: 'red' }}>{product.name}</span>
  );

  return (
    <tr>
      <td>{name}</td>
      <td>{product.price}</td>
    </tr>
  );
}

function ProductTable({ products }) {
  const rows = [];
  let lastCategory = null;

  products.forEach((product) => {
    if (product.category !== lastCategory) {
      rows.push(
        <ProductCategoryRow
          category={product.category}
          key={product.category}
        />
      );
    }
    rows.push(
      <ProductRow
        product={product}
        key={product.name}
      />
    );
    lastCategory = product.category;
  });

  return (
    <table>
      <thead>
        <tr>
          <th>Name</th>
          <th>Price</th>
        </tr>
      </thead>
      <tbody>{rows}</tbody>
    </table>
  );
}

function SearchBar() {
  return (
    <form>
      <input type="text" placeholder="Search..." />
      <label>
        <input type="checkbox" />
        {' '}
        Only show products in stock
      </label>
    </form>
  );
}

export default function FilterableProductTable({ products }) {
  return (
    <div>
      <SearchBar />
      <ProductTable products={products} />
    </div>
  );
}

自顶向下运行这段代码,一个精致完整、样式与数据就位的商品表格就已经展现在屏幕上了!


步骤三:找出 UI State 的最小且完整表示

现在要添加交互了。交互的核心是“数据随用户操作而改变”。为了让应用保持轻巧优雅,切忌定义过多重复或多余的 state。

我们把示例中出现的所有数据列出来:

  1. 原始商品列表(products);
  2. 搜索框中用户输入的文本(filterText);
  3. 复选框的勾选状态(inStockOnly);
  4. 过滤后的商品列表(filteredProducts)。

如何判断哪一个才是真正的 State?官方给出了 三条黄金法则:

  1. 它是通过 props 从父组件传递过来的吗? 如果是,那它不是 state。
  2. 它会随着时间推移保持不变吗? 如果一直不变,那它不是 state。
  3. 你能根据组件中已有的其他 state 或 props 计算出它吗? 如果能,那它绝对不应该是 state!

我们来逐一拷问:

  • 原始商品列表:从父级作为 props 传入,且不会随搜索变化,不是 state。
  • 搜索框文本:随用户键盘输入变化,且无法从别处计算出来,是 State!
  • 复选框勾选状态:随用户点击切换布尔值,无法计算,是 State!
  • 过滤后的商品列表:不是 State!因为只要拿着“原始商品列表”、“搜索文本”和“复选框状态”进行一次简单的 filter() 计算就能实时得到它!如果把它存为 state,不仅冗余,而且极易出现搜索词变了但过滤结果忘记同步的 bug。

结论:整个应用只需要两个 State 变量:filterText 与 inStockOnly!


步骤四:确定 State 放置的位置

有了 State 之后,下一个关键问题是:这个 State 应该归哪个组件管?

React 使用单向数据流。寻找 State 归属的步骤如下:

  1. 找到所有需要根据该 state 进行渲染的组件:
    • ProductTable 需要根据 filterText 和 inStockOnly 过滤列表;
    • SearchBar 需要根据它们显示当前输入框文字和复选框对勾。
  2. 找到这些组件的共同公共父组件(Common Parent Component):
    • FilterableProductTable 是它们共同的直接父级!
  3. 因此,把 state 放在 FilterableProductTable 中!

在父组件中声明:

const [filterText, setFilterText] = useState('');
const [inStockOnly, setInStockOnly] = useState(false);

然后把它们作为 props 传递给子组件:

<SearchBar filterText={filterText} inStockOnly={inStockOnly} />
<ProductTable products={products} filterText={filterText} inStockOnly={inStockOnly} />

步骤五:添加反向数据流(Inverse Data Flow)

现在数据能从顶层流向底层,但用户在底层的 SearchBar 中打字时,需要通知顶层的父组件更新 state。这就是反向数据流。

方法非常直接:父组件把改变自己 state 的函数(Setter 回调)作为 prop 传给子组件!

1. 父组件传递回调

<SearchBar
  filterText={filterText}
  inStockOnly={inStockOnly}
  onFilterTextChange={setFilterText}
  onInStockOnlyChange={setInStockOnly}
/>

2. 子组件监听原生输入事件并调用回调

function SearchBar({
  filterText,
  inStockOnly,
  onFilterTextChange,
  onInStockOnlyChange
}) {
  return (
    <form>
      <input
        type="text"
        value={filterText}
        placeholder="Search..."
        onChange={(e) => onFilterTextChange(e.target.value)}
      />
      <label>
        <input
          type="checkbox"
          checked={inStockOnly}
          onChange={(e) => onInStockOnlyChange(e.target.checked)}
        />
        {' '}
        Only show products in stock
      </label>
    </form>
  );
}

每当用户在输入框打字,触发原生的 onChange,调用父组件传来的 setFilterText,更新父组件的 filterText,父组件重新渲染并把最新数据流向 ProductTable,完成整个闭环!


总结

这就是完整的“用 React 思考”五步法:

  1. 拆解层级:画框确定组件树;
  2. 静态呈现:用纯 Props 搭建结构;
  3. 精简状态:用三法则筛选最小完整 State;
  4. 定位归属:放在公共父组件中保持单向数据流;
  5. 反向闭环:通过回调函数让子组件驱动状态更新。
下一站:你的第一个组件:声明、嵌套与大写规范 →
© 2026 React 官方教程