React编程思想

本文是对React官网《Thinking in React》一文的翻译,经过这篇文章,React团队向开发者们介绍了应该若是去构思一个web应用,为从此使用React进行web app的构建,打下基础。 如下是正文。css

在咱们团队看来,React是使用JavaScript构建大型、快速的Web apps的首选方式。它已经在Facebook和Instagram项目中,表现出了很是好的可扩展性。html

可以按照构建的方式来思考web app的实现,是React众多优势之一。在这篇文章中,咱们将引导你进行使用React构建可搜索产品数据表的思考过程。react

从设计稿开始

想象一下,咱们已经有了一个JSON API和来自设计师的设计稿。以下图所示:web

Mockup

JSON API返回的数据以下所示:json

[
  {category: "Sporting Goods", price: "$49.99", stocked: true, name: "Football"},
  {category: "Sporting Goods", price: "$9.99", stocked: true, name: "Baseball"},
  {category: "Sporting Goods", price: "$29.99", stocked: false, name: "Basketball"},
  {category: "Electronics", price: "$99.99", stocked: true, name: "iPod Touch"},
  {category: "Electronics", price: "$399.99", stocked: false, name: "iPhone 5"},
  {category: "Electronics", price: "$199.99", stocked: true, name: "Nexus 7"}
];
复制代码

第一步:将UI分解为组件并分析层级结构

咱们要作的第一件事就是给设计稿中的每一个组件(和子组件)画框,并给它们起名字。若是你正在和一个设计师合做,他可能已经帮你完成了这一步。他的Photoshop图层名称可能最终会成为你的React组件名称!数组

但咱们怎么知道本身的组件应该是什么?只须要使用一些通用的技巧来决定是否应该建立一个新的函数或对象。其中一个技巧叫作:单一责任原则。就是说,在理想状况下,一个组件应该只用来完成一件事。若非如此,则应该考虑将其分解成更小的子组件。架构

咱们常常会向用户展现JSON数据模型,那么你应该会发现,若是模型构建正确,那么你的UI(以及组件结构)应该可以很好地映射数据模型。这是由于UI和数据模型倾向于遵循相同的信息架构,这意味着将UI分解为组件的工做一般是微不足道的。如今咱们把它分解成映射数据模型的组件以下:app

Component diagram

如今咱们的示例应用中有了五个组件,并且咱们将每一个组件表明的数据用斜体表示以下:模块化

  1. **FilterableProductTable **(橘黄色):包含整个示例的组件
  2. **SearchBar **(蓝色):接收全部的用户输入
  3. **ProductTable **(绿色):根据用户输入显示和过滤数据集
  4. **ProductCategoryRow **(绿宝石色):显示分类头
  5. **ProductRow **(红色):每行显示一条商品数据

细心的你会发现,在ProductTable中,表头(包含名称价格标签)不是一个组件。这是一个偏好的问题,有两个方面的论点。在这个例子中,咱们将其做为ProductTable组件的一部分,由于它是ProductTable负责渲染的数据集的一部分。可是,若是这个头部变得很复杂(好比咱们要支持排序),那么将其设置为ProductTableHeader这样的组件确定会更好一些。函数

如今咱们已经肯定了设计稿中的组件,下一步咱们要给这些组件安排层次结构。这其实很容易:出如今一个组件中的组件应该在层次结构中显示为一个子组件:

  • FilterableProductTable
    • SearchBar
    • ProductTable
      • ProductCategoryRow
      • ProductRow

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

class ProductCategoryRow extends React.Component {
  render() {
    const category = this.props.category;
    return (
      <tr>
        <th colSpan="2">
          {category}
        </th>
      </tr>
    );
  }
}

class ProductRow extends React.Component {
  render() {
    const product = this.props.product;
    const name = product.stocked ?
      product.name :
      <span style={{color: 'red'}}>
        {product.name}
      </span>;

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

class ProductTable extends React.Component {
  render() {
    const rows = [];
    let lastCategory = null;
    
    this.props.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>
    );
  }
}

class SearchBar extends React.Component {
  render() {
    return (
      <form>
        <input type="text" placeholder="Search..." />
        <p>
          <input type="checkbox" />
          {' '}
          Only show products in stock
        </p>
      </form>
    );
  }
}

class FilterableProductTable extends React.Component {
  render() {
    return (
      <div>
        <SearchBar />
        <ProductTable products={this.props.products} />
      </div>
    );
  }
}


const PRODUCTS = [
  {category: 'Sporting Goods', price: '$49.99', stocked: true, name: 'Football'},
  {category: 'Sporting Goods', price: '$9.99', stocked: true, name: 'Baseball'},
  {category: 'Sporting Goods', price: '$29.99', stocked: false, name: 'Basketball'},
  {category: 'Electronics', price: '$99.99', stocked: true, name: 'iPod Touch'},
  {category: 'Electronics', price: '$399.99', stocked: false, name: 'iPhone 5'},
  {category: 'Electronics', price: '$199.99', stocked: true, name: 'Nexus 7'}
];
 
ReactDOM.render(
  <FilterableProductTable products={PRODUCTS} />,
  document.getElementById('container')
);
复制代码

如今咱们已经有了组件层次结构,接下来能够实现应用程序了。最初的方案是构建一个使用数据模型渲染UI但不具备交互性的版本。最好将静态版本和添加交互性进行解耦,由于构建一个静态的版本须要大量的输入却不须要思考,而增长交互性须要大量的思考而不须要不少输入。咱们一下子会知道为何。

要构建渲染数据模型的静态版本,须要构建可复用其余组件并使用props传递数据的组件。props是一种将数据从父组件传递给子组件的方式。若是你熟悉state的概念,请不要使用state来构建这个静态版本。state只为实现交互性而保留,即随时间变化的数据。因为这是应用程序的静态版本,因此暂时不须要它。

你的构建过程能够自上而下或自下而上。也就是说,你能够从构建层次较高的组件(即FilterableProductTable)开始或较低的组件(ProductRow开始)。在简单的例子中,自上而下一般比较容易,而在大型项目中,自下而上更容易并且更易于编写测试用例

在这一步的最后,你会有一个可重用组件的库来渲染你的数据模型。这些组件只会有render()方法,由于这是你的应用程序的静态版本。层次结构顶部的组件(FilterableProductTable)将把你的数据模型做为一个prop。若是你对基础数据模型进行更改并再次调用ReactDOM.render(),则UI将会更新。这就很容易看到用户界面是如何更新以及在哪里进行更改了,由于没有任何复杂的事情发生。 React的单向数据流(也称为单向绑定)使全部的事务更加模块化也更加快速。

第三步:肯定UI状态的最小(但完整)表示形式

为了使你的UI具备交互性,须要可以触发对基础数据模型的更改。 React使用state让这一切变得简单。要正确构建应用程序,首先须要考虑应用程序须要的最小可变状态集。这里的关键是:不要重复本身。找出应用程序须要的状态的绝对最小表示,并计算须要的其余全部内容。例如,若是你正在建立一个TODO列表,只须要保存一个TODO项目的数组;不要为计数保留一个单独的状态变量。相反,当你要渲染TODO数量时,只需取TODO项目数组的长度便可。

考虑咱们示例应用程序中的全部数据。咱们有:

  • 产品的原始列表
  • 用户输入的搜索文本
  • 复选框的值
  • 过滤的产品列表

咱们来看看每个是哪个state。这里有关于每条数据的三个问题:

  1. 是经过props从父组件传入的吗?若是是,那可能不是state。
  2. 它是否保持不变?若是是,那可能不是state。
  3. 你能基于组件中的任何其余state或props来计算它吗?若是是,那不是state。

原来的产品清单是做为props传入的,因此这不是state。搜索文本和复选框彷佛是state,由于它们随着时间而改变,不能从任何东西计算。最后,产品的过滤列表不是state,由于它能够经过将产品的原始列表与复选框的搜索文本和值组合来计算获得。

因此最后,咱们的states是:

  • 用户输入的搜索文本
  • 复选框的值

第四步: 肯定你的state须要放置在什么地方

class ProductCategoryRow extends React.Component {
  render() {
    const category = this.props.category;
    return (
      <tr>
        <th colSpan="2">
          {category}
        </th>
      </tr>
    );
  }
}

class ProductRow extends React.Component {
  render() {
    const product = this.props.product;
    const name = product.stocked ?
      product.name :
      <span style={{color: 'red'}}>
        {product.name}
      </span>;

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

class ProductTable extends React.Component {
  render() {
    const filterText = this.props.filterText;
    const inStockOnly = this.props.inStockOnly;

    const rows = [];
    let lastCategory = null;

    this.props.products.forEach((product) => {
      if (product.name.indexOf(filterText) === -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>
    );
  }
}

class SearchBar extends React.Component {
  render() {
    const filterText = this.props.filterText;
    const inStockOnly = this.props.inStockOnly;

    return (
      <form>
        <input
          type="text"
          placeholder="Search..."
          value={filterText} />
        <p>
          <input
            type="checkbox"
            checked={inStockOnly} />
          {' '}
          Only show products in stock
        </p>
      </form>
    );
  }
}

class FilterableProductTable extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      filterText: '',
      inStockOnly: false
    };
  }

  render() {
    return (
      <div>
        <SearchBar
          filterText={this.state.filterText}
          inStockOnly={this.state.inStockOnly}
        />
        <ProductTable
          products={this.props.products}
          filterText={this.state.filterText}
          inStockOnly={this.state.inStockOnly}
        />
      </div>
    );
  }
}


const PRODUCTS = [
  {category: 'Sporting Goods', price: '$49.99', stocked: true, name: 'Football'},
  {category: 'Sporting Goods', price: '$9.99', stocked: true, name: 'Baseball'},
  {category: 'Sporting Goods', price: '$29.99', stocked: false, name: 'Basketball'},
  {category: 'Electronics', price: '$99.99', stocked: true, name: 'iPod Touch'},
  {category: 'Electronics', price: '$399.99', stocked: false, name: 'iPhone 5'},
  {category: 'Electronics', price: '$199.99', stocked: true, name: 'Nexus 7'}
];

ReactDOM.render(
  <FilterableProductTable products={PRODUCTS} />,
  document.getElementById('container')
);
复制代码

如今咱们已经肯定了最小的一组应用程序state。接下来,咱们须要肯定哪一个组件会改变或拥有这个state。

请记住:数据在React的组件层次结构中是单向流动的。它可能不清楚哪一个组件应该拥有什么状态。这一般是新手理解的最具挑战性的部分,因此请按照如下步骤解决:

对于你的应用程序中的每个state:

  • 肯定基于该state渲染某些内容的每一个组件。
  • 找到一个共同的拥有者组件(一个在全部须要该state的层次结构组件之上的组件)。
  • 不管是共同全部者,仍是高层次的其余组成部分,都应该拥有这个state。
  • 若是你没法找到一个有意义的组件,那么只好建立一个新的组件来保存state,并将其添加到公共全部者组件上方的层次结构中的某个位置。

让咱们来看看咱们的应用程序的这个策略:

  • ProductTable须要根据状态过滤产品列表,而SearchBar须要显示搜索文本和检查状态。
  • 通用全部者组件是FilterableProductTable
  • 从概念上讲,过滤器文本和选中的值存在于FilterableProductTable中是有意义的

酷,因此咱们已经决定,咱们的state存活在FilterableProductTable中。首先,将一个实例属性this.state = {filterText:'',inStockOnly:false}添加到FilterableProductTable的构造函数中,以反映应用程序的初始状态。而后,将filterTextinStockOnly做为prop传递给ProductTableSearchBar。最后,使用这些props来筛选ProductTable中的行,并在SearchBar中设置表单域的值。

你能够看到你的应用程序的行为了:设置filterText为“ball”,并刷新你的应用程序。你将看到数据表已正确更新。

第五步:添加反向数据流

class ProductCategoryRow extends React.Component {
  render() {
    const category = this.props.category;
    return (
      <tr>
        <th colSpan="2">
          {category}
        </th>
      </tr>
    );
  }
}

class ProductRow extends React.Component {
  render() {
    const product = this.props.product;
    const name = product.stocked ?
      product.name :
      <span style={{color: 'red'}}>
        {product.name}
      </span>;

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

class ProductTable extends React.Component {
  render() {
    const filterText = this.props.filterText;
    const inStockOnly = this.props.inStockOnly;

    const rows = [];
    let lastCategory = null;

    this.props.products.forEach((product) => {
      if (product.name.indexOf(filterText) === -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>
    );
  }
}

class SearchBar extends React.Component {
  constructor(props) {
    super(props);
    this.handleFilterTextChange = this.handleFilterTextChange.bind(this);
    this.handleInStockChange = this.handleInStockChange.bind(this);
  }
  
  handleFilterTextChange(e) {
    this.props.onFilterTextChange(e.target.value);
  }
  
  handleInStockChange(e) {
    this.props.onInStockChange(e.target.checked);
  }
  
  render() {
    return (
      <form>
        <input
          type="text"
          placeholder="Search..."
          value={this.props.filterText}
          onChange={this.handleFilterTextChange}
        />
        <p>
          <input
            type="checkbox"
            checked={this.props.inStockOnly}
            onChange={this.handleInStockChange}
          />
          {' '}
          Only show products in stock
        </p>
      </form>
    );
  }
}

class FilterableProductTable extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      filterText: '',
      inStockOnly: false
    };
    
    this.handleFilterTextChange = this.handleFilterTextChange.bind(this);
    this.handleInStockChange = this.handleInStockChange.bind(this);
  }

  handleFilterTextChange(filterText) {
    this.setState({
      filterText: filterText
    });
  }
  
  handleInStockChange(inStockOnly) {
    this.setState({
      inStockOnly: inStockOnly
    })
  }

  render() {
    return (
      <div>
        <SearchBar
          filterText={this.state.filterText}
          inStockOnly={this.state.inStockOnly}
          onFilterTextChange={this.handleFilterTextChange}
          onInStockChange={this.handleInStockChange}
        />
        <ProductTable
          products={this.props.products}
          filterText={this.state.filterText}
          inStockOnly={this.state.inStockOnly}
        />
      </div>
    );
  }
}


const PRODUCTS = [
  {category: 'Sporting Goods', price: '$49.99', stocked: true, name: 'Football'},
  {category: 'Sporting Goods', price: '$9.99', stocked: true, name: 'Baseball'},
  {category: 'Sporting Goods', price: '$29.99', stocked: false, name: 'Basketball'},
  {category: 'Electronics', price: '$99.99', stocked: true, name: 'iPod Touch'},
  {category: 'Electronics', price: '$399.99', stocked: false, name: 'iPhone 5'},
  {category: 'Electronics', price: '$199.99', stocked: true, name: 'Nexus 7'}
];

ReactDOM.render(
  <FilterableProductTable products={PRODUCTS} />,
  document.getElementById('container')
);
复制代码

到目前为止,咱们已经构建了一个应用程序,能够根据props和state正确地呈如今层次结构中。如今是时候以另外一种方式支持数据流:深层次的表单组件须要更新FilterableProductTable中的状态。

React使这个数据流清晰易懂,以便理解你的程序是如何工做的,可是它须要比传统的双向数据绑定更多的输入。

若是你尝试在当前版本的示例中键入或选中该框,则会看到React忽略了你的输入。这是故意的,由于咱们已经将输入的值prop设置为始终等于从FilterableProductTable传入的state。

让咱们想一想咱们想要发生的事情。咱们但愿确保每当用户更改表单时,咱们都会更新状态以反映用户的输入。因为组件应该只更新本身的state,只要state须要更新时,FilterableProductTable就会传递回调到SearchBar。咱们可使用输入上的onChange事件来通知它。 FilterableProductTable传递的回调将调用setState(),而且应用程序将被更新。

虽然这听起来很复杂,但实际上只是几行代码。你的数据如何在整个应用程序中流动变得很是明确。

就是这样

但愿这篇文章可让你了解如何用React来构建组件和应用程序。虽然它可能比之前多一些代码,但请记住,代码的读远远超过它的写,而且读取这个模块化的显式代码很是容易。当你开始构建大型组件库时,你将会体会到这种明确性和模块性,而且经过代码重用,你的代码行将开始缩小。

备注

文中全部示例的HTML和CSS内容以下:

<div id="container">
    <!-- This element's contents will be replaced with your component. -->
</div>
复制代码
body {
  padding: 5px
}
复制代码
相关文章
相关标签/搜索