11.Thinking in React (用React思考)

React版本:15.4.2
**翻译:xiyoki **

在我们看来,React是使用Javascript构建大型,快速的Web应用程序的首要方式。它在我们的Facebook和Instagram上已经非常好。
React许多重要的部分之一是它如何让你在构建应用程序时进行思考。在本文档中,我们将向你介绍使用React构建可搜索的产品数据表的思考过程。

Start With A Mock(从Mock开始)

想象一下,我们已经有了一个JSON API和设计师提供的mock。Mock看起来像这样:



我们的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"}
];

Step 1: Break The UI Into A Component Hierarchy(步骤1:将UI拆分为一个组件层次结构)

你要做的第一件事是为mock中每个组件(和子组件)的周围绘制框,并为所有的框命名。如果你正在与一个设计师工作,他们可能已经这样做了,所以去和他们谈谈吧!他们的Photoshop图层名称最终可能是你的React组件的名称。
但你怎样知道什么能自成为一个组件呢?只需使用相同的技术来决定是否应该创建一个新的函数或对象即可。其中一个技术是单一职责原则,即理想情况下,组件应该只做一件事。如果它最终生长,它应该被分解为更小的子组件。
由于你经常向用户展示JSON数据模型,你会发现如果你的模型构建正确,你的UI(即你的组件结构)将会很好地映射。这是因为UI和数据模型倾向于遵循相同的信息架构,这意味着将UI划分为组件的工作通常是微不足道的。只需要将UI分解成一个完全代表数据模型的组件即可。



你会看到,在我们简单的应用程序中有五个组件。我们用斜体表示每个组件所代表的数据。

  1. FilterableProductTable (橙色): 包含全部示例
  1. SearchBar (蓝色): 接收所有用户输入
  2. ProductTable (绿色): 根据用户输入显示和过滤数据集
  3. ProductCategoryRow (绿松石): 显示每个类别的标题
  4. ProductRow (红色): 每个产品显示一行

如果你看看ProductTable,你会看到表头(包含名称和价格的标签)不自成为一个组件。这是一个偏好问题,也是一个争论。就这个示例而言,我们将它作为ProductTable的一部分是因为它是数据集渲染的一部分,而渲染数据集是ProductTable的职责。然而,如果这个头部变得复杂(即,如果我们添加排序功能),则使其自成为ProductTableHeader组件是有意义的。
现在我们已经在我们的mock中识别了组件,让我们将它们排列成层次结构。这很容易。在mock中,出现在另一个组件中的组件应该在层次结构中显示为子级:

  • FilterableProductTable
  • SearchBar
  • ProductTable
  • ProductCategoryRow
  • ProductRow

Step 2: Build A Static Version in React(步骤2:在React中创建一个静态版本)

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

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

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

var 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是将数据从父级传递到子级的一种方式。如果你熟悉状态的概念,不要使用状态来构建这个静态的版本。状态仅保留用于交互性,即随时间变化的数据。由于这是一个静态版本的应用程序,你不需要它。
你可以自顶向下或自下而上的构建。也就是说,你可以从层次结构中较高的组件开始(即从FilterableProductTable开始)或从较低的组件开始(ProductRow)。在更简单的例子中,通常更容易自上而下,在更大的项目中,更容易从底层向上编写和测试。
在此步骤结束时,你将有一个可重用的组件库,用于呈现你的数据模型。因为这是一个静态版本的应用程序,所以组件只用render()方法。位于层次结构顶部的组件(FilterableProductTable)将把你的数据模型作为props。如果对基础数据模型进行更改并再次调用ReactDOM.render(),则UI将更新。很容易看到你的UI是如何更新的,在哪里做更改,因为没有什么复杂的。React的单向数据流(也称为单向绑定)使一切模块化和快速。

A Brief Interlude: Props vs State(一个简短的插曲:Props vs State)

React中有两种类型的‘模型’数据:props和state。重要的是要了解二者之间的区别。

Step 3: Identify The Minimal (but complete) Representation Of UI State(步骤3:识别UI状态的最小(但完整)表示)

要使你的UI交互,你需要能够触发对基础数据模型的更改。React 状态使这个很容易。
要正确构建你的应用程序,你首先需要考虑你的应用需要的最小可变状态集。这里的关键是DRY:不要重复自己。确定你的应用程序需要的状态的绝对最小表示,并计算其他所有你需要的内容。例如,你正在构建一个TODO列表,只需要保留一个TODO项数组;不要为计数保留单独的状态变量。相反,当你想渲染TODO计数时,只需取TODO项数组的长度。
想想我们应用程序中的所有数据。我们有:

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

让我们一 一过一遍,找出哪一个是状态。只需对每个数据询问三个问题:

  1. 该数据是从父级props变量传递进来的吗?如果是这样,它可能不是状态。
  1. 该数据是否随时间保持不变?如果是这样,它可能不是状态。
  2. 你可以根据组件中其他任何状态或props来计算该数据吗?如果是这样,它可能不是状态。

原始的产品列表作为props传递,所以不是状态。搜索文本和复选框似乎是状态,因为它们随时间变化,不从任何其他数据计算而来。最后,过滤的产品列表不是状态,因为它可以通过将原始产品列表与搜索文本和复选框的值组合来计算。
所以最后,状态有:

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

Step 4: Identify Where Your State Should Live(步骤4:识别状态应该处在什么位置上)

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

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

class ProductTable extends React.Component {
  render() {
    var rows = [];
    var lastCategory = null;
    this.props.products.forEach((product) => {
      if (product.name.indexOf(this.props.filterText) === -1 || (!product.stocked && this.props.inStockOnly)) {
        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() {
    return (
      <form>
        <input type="text" placeholder="Search..." value={this.props.filterText} />
        <p>
          <input type="checkbox" checked={this.props.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>
    );
  }
}

var 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')
); 

OK,我们已经确定了什么是最小的应用程序状态集。接下来,我们需要确定哪个组件变动或拥有此状态。

记住:React就是单向数据流的组件层次结构。

哪个组件应该拥有什么状态可能不是立即就能清楚的。对于新手来说,这通常是最具有挑战性的部分,因此请按以下步骤了解:

对于应用程序中的每个状态:

  • 标识基于该状态渲染某物的每个组件。
  • 找到一个公共所有者组件(在层次结构中,一个在所有组件之上并且需要状态的单个组件)。
  • 公共所有者或层次结构中较高的另一组件应该拥有该状态。
  • 如果你找不到一个拥有该状态是有意义的组件,那么就创建一个只是为了保持状态的新组件,并将其添加到公共所有者组件上层的某个位置。

让我们来运行一下这个策略:

  • productTable需要基于状态过滤产品列表,并且SearchBar需要显示搜索文本和选中状态。
  • 公共所有者组件是FilterableProductTable
  • 过滤文本和选中值存在于FilterableProductTable上,这从概念上来说是有意义的。

酷。所以我们决定让状态存在于FilterableProductTable上。
首先,将这个实例属性this.state={filterText: '', inStockOnly: false}添加到FilterableProductTableconstructor,以此来反映应用程序的初始状态。
然后,通过将filterTextinStockOnly作为prop传递到ProductTableSearchBar
最后,用这些props来过滤ProductTable中的行,并且设置表单字段inSearchBar 的值。
此刻你可以看到你的应用程序将如何表现:为ball设置filterText并且刷新你的应用程序。你将看到数据表已正确更新。

Step 5: Add Inverse Data Flow (步骤5:添加反向数据流)

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

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

class ProductTable extends React.Component {
  render() {
    var rows = [];
    var lastCategory = null;
    this.props.products.forEach((product) => {
      if (product.name.indexOf(this.props.filterText) === -1 || (!product.stocked && this.props.inStockOnly)) {
        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.handleChange = this.handleChange.bind(this);
  }
  
  handleChange() {
    this.props.onUserInput(
      this.filterTextInput.value,
      this.inStockOnlyInput.checked
    );
  }
  
  render() {
    return (
      <form>
        <input
          type="text"
          placeholder="Search..."
          value={this.props.filterText}
          ref={(input) => this.filterTextInput = input}
          onChange={this.handleChange}
        />
        <p>
          <input
            type="checkbox"
            checked={this.props.inStockOnly}
            ref={(input) => this.inStockOnlyInput = input}
            onChange={this.handleChange}
          />
          {' '}
          Only show products in stock
        </p>
      </form>
    );
  }
}

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

  handleUserInput(filterText, inStockOnly) {
    this.setState({
      filterText: filterText,
      inStockOnly: inStockOnly
    });
  }

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


var 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忽略了你的输入。这是有意的,因为我们已设置input 的value prop总是等于从FilterableProductTable传递过来的state。
让我们想想,我们想要发生什么。我们要确保每当用户更改表单时,我们将更新状态以反映用户输入。由于组件应该只更新自己的状态,FilterableProductTable将向SearchBar传递回调函数,该回调函数将在状态更新时触发。我们可以将input的onChange事件作为它的通知。由FilterableProductTable传递的回调函数将会调用setState(),应用程序将被更新。
虽然这听起来很复杂,但它只是几行代码。数据如何在整个应用程序中流动已非常明确。

And That's It

希望,这给了你如何使用React来构建组件和应用程序的想法。虽然它可能比你常用的方法更多的打字,记住代码的可读性远远超过它的书写,并且阅读这个模块化、明确性的代码是非常容易的。当你开始构建大型的组件库时,你会欣赏这种明确性和模块性,并且使用代码重用,你的代码行将开始收缩。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,413评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,591评论 18 139
  • 原教程内容详见精益 React 学习指南,这只是我在学习过程中的一些阅读笔记,个人觉得该教程讲解深入浅出,比目前大...
    leonaxiong阅读 2,809评论 1 18
  • http://www.chaishubang.com/memoirs/memoirsinfo/1969
    Fly_Catkin阅读 202评论 0 0
  • 自由从来不是人性的必需品,而爱才是 文/半个错别字 第88届奥斯卡的几部获奖电影看下来,如《荒野猎人》、《聚焦》、...
    半个错别字阅读 764评论 1 0