React 编程思想 #2

发布时间 2023-03-31 17:11:24作者: 風栖祈鸢

React 编程思想 #2

接上文,已经实现了一个静态的页面,现在就要给页面加上交互了。

寻找 State

状态是应用需要记录的最小变化,构建状态的最重要的原则是 DRY(Don’t Repeat Yourself,不要重复自己)。对于一个应用,构建出它的状态的绝对最小表示,并通过这些状态计算其他需要的内容。例如,如果您正在构建一个购物列表,则可以将项目存储为状态中的数组。如果您还想显示列表中的项数,请不要将项数存储为另一个状态值,而是读取数组的长度。

以上来自 React 的官方文档对状态的定义,简单来说,状态是组件中不可计算的变量,当这些变量改变时,需要动态改变页面的内容,即重新渲染页面。

现在回想示例程序中的所有数据:

  • 原始的产品列表;
  • 用户输入的搜索文本;
  • 单选框的值;
  • 经过筛选的产品列表;

这里面哪些属于状态?状态需要满足以下要求:

  • 随着时间推移,它会发生改变;
  • 它不能是由父组件通过 props 传递来的;
  • Don’t Repeat Yourself,它不应该能从任何地方计算出来;

按照这个要求,我们可以进行筛选:

  • 原始的产品列表是由 props 传递的,所以它不是状态;
  • 用户输入的搜索文本会随着时间改变(用户改变),并且无法根据任何内容进行计算;
  • 单选框的值会随着时间改变(用户改变),并且无法根据任何内容进行计算;
  • 经过筛选的产品列表可以由原始的产品列表、单选框的值、输入的搜索文本计算出来(筛选),因此它也不是状态;

所以,只有搜索文本和单选框的值才是状态,Nice Done!

React 中有两种类型的“模型”数据:props 和 state。两者区别很大:
props 就像传递给函数的参数。它们允许父组件将数据传递给子组件并自定义其外观。例如,Form 可以将颜色 props 传递给 Button。
state 就像一个组件的内存。它允许组件跟踪一些信息,并根据交互对其进行更改。例如,按钮可能会跟踪 isHovered 状态。
props 和 state 是不同的,但它们是协同工作的。父组件通常会将一些信息保持在 state (可以进行改变),并将其作为子组件的 state 传递给子组件。如果第一次阅读时仍然感觉不清楚,那也没关系。它需要一点练习才能真正坚持下来!

决定 State 的位置

在确定了应用的状态后,还需要确定哪个组件负责拥有和更改这些状态。记住:React使用单向数据流,数据自顶向下流动,数据从父组件向下传递到子组件。目前可能还不清楚哪个组件应该拥有什么状态,如果你是这个概念的新手,这可能很有挑战性,但可以按照以下步骤来解决:

对于应用中的每个状态:

  1. 识别基于该状态呈现某些内容的每个组件;

  2. 找到它们最接近的公共父组件————在层次结构中位于它们之上的组件。

  3. 决定这个组件所在的位置:

通常,可以将状态直接放入它们的公共父级中,也可以将状态放入其公共父级之上的某个组件中。

如果找不到拥有状态的组件,请创建一个仅用于保存状态的新组件,并将其添加到公共父组件上方层次结构中的某个位置。

现在回到这个例子中的状态,决定它们的位置:

识别使用状态的组件:

  • ProductTable 需要根据该状态(搜索文本和单选框的值)筛选产品列表。

  • SearchBar 需要显示该状态(搜索文本和单选框的值)。

找到它们的共同父组件:两个组件共享的第一个父组件是 FilterableProductTable。

决定状态的位置:可以在 FilterableProductTable 中保存搜索文本和单选框的状态值。

因此,状态值将存在于 FilterableProductTable 中,代码如下:

  function FilterableProductTable({ products }) {
    const [filterText, setFilterText] = React.useState('');
    const [inStockOnly, setInStockOnly] = React.useState(false);

这里需要提一下,useState 方法只在 React 16.8 版本以上存在,由于我之前使用的是 16.4 版本,这地方一直报错方法不存在,研究了半天才发现问题。

接着,将 State filterText 和 inStockOnly 作为 props 传递给 ProductTable 和 SearchBar:

function FilterableProductTable({ products }) {
  const [filterText, setFilterText] = useState('');
  const [inStockOnly, setInStockOnly] = useState(false);

  return (
    <div>
      <SearchBar 
        filterText={filterText} 
        inStockOnly={inStockOnly} />
      <ProductTable 
        products={products}
        filterText={filterText}
        inStockOnly={inStockOnly} />
    </div>
  );
}

当 filterText 或 inStockOnly 状态改变时(用户进行了操作),拥有这些状态的组件 FilterableProductTable 会感知到状态的变化,重新渲染页面内容;又因为 ProductTable 和 SearchBar 组件在 FilterableProductTable 组件中,同时接收了这两个状态作为 props,它们也会重新渲染。这样,ProductTable 和 SearchBar 就可以根据状态更改自身显示的内容了。

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

  products.forEach((product) => {
    if (
      product.name.toLowerCase().indexOf(
        filterText.toLowerCase()
      ) === -1
    ) {
      return;
    }
    if (inStockOnly && !product.stocked) {
      return;
    }
    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({ filterText, inStockOnly }) {
  return (
    <form>
      <input 
        type="text" 
        value={filterText} 
        placeholder="Search..."/>
      <label>
        <input 
          type="checkbox" 
          checked={inStockOnly} />
        {' '}
        Only show products in stock
      </label>
    </form>
  );
}

其中,SearchBar 组件根据这两个状态,更改页面上显示的内容;ProductTable 组件根据这两个状态,重新计算经过筛选的产品列表,如此便实现了页面的动态变化。

添加反向数据流

目前,应用已经通过自顶向下流动的 props 和 state 进行了正确的渲染。但是,要想根据用户输入更改状态,还需要支持数据以另一种方式流动:位于层次结构中下方的表单组件需要更新 FilterableProductTable 中的状态。

React明确地规定了数据流动的方式,但相比于双向数据绑定,它需要更多的输入(代码编写)。如果尝试在上面的组件构成的页面中输入搜索内容或改变单选框,可以看到输入没有响应,被 React 忽略了。这是故意的(?)。由于代码中的 <input value={filterText}/>,搜索框的值始终等于从 FilterableProductTable 传入的 filterText 状态。由于 filterText 状态初始为空,且从未被 setStatu 方法设置(只有这个方法才能改变状态,React 也是从这感知到了状态的变化),因此搜索框的值永远不会更改(单选框同理)。

我们需要的效果是,当用户更改表单输入时,状态都会更新并反映这些更改。该状态由 FilterableProductTable 所有,因此只有它才能调用 setFilterText 和 setInStockOnly。为了能让 SearchBar 更新 FilterableProductTable 的状态,需要将改变状态的函数传递给 SearchBar:

function FilterableProductTable({ products }) {
  const [filterText, setFilterText] = useState('');
  const [inStockOnly, setInStockOnly] = useState(false);

  return (
    <div>
      <SearchBar 
        filterText={filterText} 
        inStockOnly={inStockOnly} 
        onFilterTextChange={setFilterText} 
        onInStockOnlyChange={setInStockOnly} />
      <ProductTable 
        products={products} 
        filterText={filterText}
        inStockOnly={inStockOnly} />
    </div>
  );
}

可以理解为,应用已经完成了自顶向下的数据流动,但用户位于底层,用户的操作需要自底向上传递给应用。通过将改变状态的函数作为引用传递给 SearchBar,它也拥有了改变状态的能力,因此,SearchBar -> FilterableProductTable 的流动被打通了。而用户与 SearchBar 之间的流动,就是简单的事件响应了,在 SearchBar 中添加如下事件:

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

这样,用户操作 -> SearchBar 的流向也被打通了。因此,用户操作 -> SearchBar -> FilterableProductTable 的自底向上的流向被打通了:

//import useState from 'react';

function FilterableProductTable({ products }) {
    const [filterText, setFilterText] = React.useState('');
    const [inStockOnly, setInStockOnly] = React.useState(false);
  
    return (
      <div>
        <SearchBar 
          filterText={filterText} 
          inStockOnly={inStockOnly} 
          onFilterTextChange={setFilterText} 
          onInStockOnlyChange={setInStockOnly} />
        <ProductTable 
          products={products} 
          filterText={filterText}
          inStockOnly={inStockOnly} />
      </div>
    );
  }
  
  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, filterText, inStockOnly }) {
    const rows = [];
    let lastCategory = null;
  
    products.forEach((product) => {
      if (
        product.name.toLowerCase().indexOf(
          filterText.toLowerCase()
        ) === -1
      ) {
        return;
      }
      if (inStockOnly && !product.stocked) {
        return;
      }
      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({
    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>
    );
  }
  
  const PRODUCTS = [
    {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"}
  ];

function App() {
    return <FilterableProductTable products={PRODUCTS} />;
  }

对页面操作的声音从山底传向了山顶,虽然微不足道,但已足以引发山顶的数据瀑布倾盆而下。