以 React 的方式思考

这是 React 官方文档中的一章,为了加深理解所以翻译出来,原文在这儿。


React 很棒的一点是创建应用中引导你思考的过程。这篇文档中,我们将通过运用React创建一个产品搜索列表,来引导你熟悉这个思考过程。

开始

假设我们已经有了一个JSON API和前端工程师设计的界面,如下面这样:


以 React 的方式思考_第1张图片
图片.png

我们的JSON API返回的数据是这个样子:

[
  {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"}
];

第一步:把界面分解为部件层次

很可能你要做的第一件事,是在每个部件(子部件)周围画方框并为它们取名字。如果你和一名设计师一起工作,很可能他们已经这样做了。那么去和他们聊聊,或许他们Photoshop中图层的名字直接可以作为你的React部件的名字呢!

但你怎样定义一个部件呢?你日常编程中怎样决定创建一个函数或对象的?道理相同。一个类似的技术是功能单一原则(single responsibility principle), 意思是,一个部件应该只做一件事情。如果它越来越大,那么它应该被分为更小的部件。

由于你常常将JSON数据展示给用户看,你会发现,如果数据模型建得不错,你的UI(与你的部件结构)也相应的不会太差。原因是UI和数据模型往往依赖相同的信息架构,这也意味着把UI分解为部件常常不是太难,不过是根据数据模型来分解罢了。

以 React 的方式思考_第2张图片
图片.png

你会看到我们这个简单的示例程序里有5个部件。

  1. FilterableProductTable(橙色):整个示例程序
  2. SearchBar(蓝色):接收所有的用户输入
  3. ProductTable(绿色):根据用户输入显示和过滤数据
  4. ProductCategoryRow(青绿色):显示类别
  5. ProductRow(红色):显示产品行

如果仔细看ProductTable,会发现表头(Name和Price)不是它自己的部件。这是个见仁见智的问题,使用哪种方式还有争论。这个例子中,我们把它作为ProductTable的一部分,因为渲染数据集是ProductTable的责任。然而,如果这个表头过于复杂(如果以后我们增加点击表头排序),当然应该作为一个独立的部件ProductTableHeader来创建。

现在我们在原型中已经明确了部件,接下来把它们按照层次结构组织起来。原型中一个部件在另一个部件中,层次结构中应该为父子层级关系:

  • FilterableProductTable
    • SearchBar
    • ProductTable
      • ProductCategoryRow
      • ProductRow

第二步:建立静态版本

class ProductCategoryRow extends React.Component {
  render() {
    const category = this.props.category;
    return (
      
        
          {category}
        
      
    );
  }
}

class ProductRow extends React.Component {
  render() {
    const product = this.props.product;
    const name = product.stocked ?
      product.name :
      
        {product.name}
      ;

    return (
      
        {name}
        {product.price}
      
    );
  }
}

class ProductTable extends React.Component {
  render() {
    const rows = [];
    let lastCategory = null;
    
    this.props.products.forEach((product) => {
      if (product.category !== lastCategory) {
        rows.push(
          
        );
      }
      rows.push(
        
      );
      lastCategory = product.category;
    });

    return (
      {rows}
Name Price
); } } class SearchBar extends React.Component { render() { return (

{' '} Only show products in stock

); } } class FilterableProductTable extends React.Component { render() { return (
); } } 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( , document.getElementById('container') );

现在你有了部件层级,是时候实现应用了。最容易的方法是先建立一个获取数据、渲染UI但没有交互的版本。把这些过程分离出来,是因为建立静态版本需要很多输入操作但不需要过多思考,增加交互功能不需要太多输入但需要很多思考。接下来我们会看到我这么说的原因。

建立渲染数据模型的静态版本,你需要创建使用其他部件的部件并且用props来传递数据。props是从父部件向子部件传递数据的一种方法。如果你对状态state的概念熟悉,在创建应用的静态版本时一定别使用state。状态只保留在交互的时候用。

你可以由底向上或从上到底开始。或者说,你可以首先创建最顶层的部件(例如从FilterableProductTable开始)或首先创建最底层部件(从ProductRow开始)。在简单的应用中,一般采取由上到底的方式;复杂的应用为了便于边创建边测试则相反。

这一步结束的时候,你会有了一个渲染数据模型的可重用部件库。因为这是应用的静态版,部件只包含render()方法。最顶层的部件(FilterableProductTable)或取数据模型为prop。如果数据模型中的数据有改变,重新调用render(),UI会相应的更新。静态版本复杂性不高,会很容易的看到UI如何更新。React单向数据流(one-way data flowone-way-binding)保证了模块化和相应速度。

属性(Props)和状态(State)的插曲

React中有两种模型数据:props和state。理解两者之间的区别非常重要;进一步了解请参考官方文档。

第三步:确定最少(但功能齐全)的UI状态

使UI具备交互功能,需要底层数据触发事件。React的状态state让这一点的实现很简单。

为了正确地创建应用,要首先思考应用需要的最小的状态变化。关键是别重复造轮子——DRY: Don’t Repeat Yourself. 找出应用需要的最少的数据,据此在计算其他的。例如,如果要创建TODO列表,只要有个保存TODO项目的数组即可,不需要TODO项目数量的数据。因为数量可以由获取数组长度很容易地得到。

考虑我们这个例子中需要的数据,我们有了:

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

我们逐一分析,看看哪个是状态。对每一个数据,只要问三个问题:

  1. 它是父部件经由props传递给子部件的吗?如果是,很可能不是状态。
  2. 它的值在应用操作过程中会改变吗?如果不会,很可能不是状态。
  3. 它的值能由其他状态或属性计算得到吗?如果是,很可能不是状态。

原始数据列表经props传入,那它不是状态。搜索文本和复选框的值会在应用操作过程中被改变,而且不能由其他属性或状态计算获得,看起来是状态。最后,过滤的产品列表不是状态,因为它可以经过计算原始数据列表、搜索文本和复选框的值获得。

最后,我们的状态是:

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

第四步:确定状态的位置

class ProductCategoryRow extends React.Component {
  render() {
    const category = this.props.category;
    return (
      
        
          {category}
        
      
    );
  }
}

class ProductRow extends React.Component {
  render() {
    const product = this.props.product;
    const name = product.stocked ?
      product.name :
      
        {product.name}
      ;

    return (
      
        {name}
        {product.price}
      
    );
  }
}

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(
          
        );
      }
      rows.push(
        
      );
      lastCategory = product.category;
    });

    return (
      {rows}
Name Price
); } } class SearchBar extends React.Component { render() { const filterText = this.props.filterText; const inStockOnly = this.props.inStockOnly; return (

{' '} Only show products in stock

); } } class FilterableProductTable extends React.Component { constructor(props) { super(props); this.state = { filterText: '', inStockOnly: false }; } render() { return (
); } } 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( , document.getElementById('container') );

我们确定了应用中的最少的状态,接下来,我们确定这些状态属于哪个部件。

记住:React的部件中数据是单向由顶向下流动。哪些部件传递这些状态可能不能马上弄清楚。这往往是新手理解起来最难的部分,按照下面的流程确定:

对于应用中每一个状态:

  • 确定依赖这个状态来渲染的每一个部件
  • 寻找共同的父部件(在部件层级中,位于所有需要这个状态的部件之上的父部件)
  • 或者拥有这些状态的层级更高的部件
  • 如果找不到拥有这个状态的部件,创建一个持有这个状态的新部件,加到部件层级中,位置在共同父部件之上。

我们根据上面的原则检视一下:

  • ProductTable需要根据状态过滤产品,SearchBar需要显示搜索文本和复选框状态
  • 它们共同的父部件是FilterableProductTable
  • 过滤文本和复选框值放在FilterableProductTable看起来有意义

酷,那么我们决定把状态放在FilterableProductTable中。首先,在FilterableProductTable构造器constructor中增加this.state = {filterText: '', inStockOnly: false}来设置应用的初始状态。接着,将filterTextinStockOnly作为属性传递到ProductTable和SearchBar中。最后,用这些属性过滤ProductTable的数据,同时显示在SearchBar表单中。

你会开始看到应用如何反应:设置filterText“ball”然后刷新应用。你会看到数据表正确地刷新了。

第五步:添加反向数据流

class ProductCategoryRow extends React.Component {
  render() {
    const category = this.props.category;
    return (
      
        
          {category}
        
      
    );
  }
}

class ProductRow extends React.Component {
  render() {
    const product = this.props.product;
    const name = product.stocked ?
      product.name :
      
        {product.name}
      ;

    return (
      
        {name}
        {product.price}
      
    );
  }
}

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(
          
        );
      }
      rows.push(
        
      );
      lastCategory = product.category;
    });

    return (
      {rows}
Name Price
); } } 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 (

{' '} Only show products in stock

); } } 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 (
); } } 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( , document.getElementById('container') );

现在为止,我们创建的这个应用能够根据属性和状态正确地渲染。现在是时候支持反向数据流了:在部件层级内部的表单需要更新FilterableProductTable状态。

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

如果你尝试在当前版本的示例中键入或选中该框,则会看到React忽略了你的输入。这是因为我们已经将输入的值prop设置为始终等于从FilterableProductTable传入的状态。

让我们想想我们希望发生的事。我们希望确保每当用户更改表单时,我们都会更新状态以反映用户的输入。由于组件应该只更新自己的状态,FilterableProductTable会将回调传递给SearchBar,只要状态更新就会触发。我们可以使用输入上的onChange事件来通知它。FilterableProductTable传递的回调将调用setState(),应用将被更新。

虽然这听起来很复杂,实际上只是几行代码。这真的使数据如何在整个应用程序中如何流动一目了然。

结语

希望这可以让你了解如何用React来构建组件和应用。 尽管可能需要会比以前更多地输入内容,但请记住,代码的可读性远远比代码的编写重要,读取模块化的显式代码非常容易。当你开始构建大型组件库时,将会体会到这种明确性和模块性,通过代码重用,你的代码行将开始缩小。

你可能感兴趣的:(以 React 的方式思考)