React中文文档之Thinking in React

Thinking in React - 思考React
在我们看来,React是使用js来创建大的、速度快的web应用的首选方式。它已经在Facebook和Instagram表现的非常好。
React众多伟大部分之一是:创建应用时,它是如何让你思考的。在这个文档中,我们将带领你穿越,使用React来构建搜索产品数据表的整个思维过程


Start With A Mock - 以一个模拟开始
想象一下,我们已经有一个JSON API和来自我们设计师的一个模拟。我们的设计显然不是很好,因为模拟看起来就像下面这样:

我们的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 - 第一步:打破UI,成为一个组件层次结构
你将做的第一件事情是,在模拟环境下给每个组件(和子组件)周围画上方框,并给定它们名称。如果你正在和一个设计师合作,他们可能已经做了这个,因此去和他们交流!他们的PS的图层名可能就是你的React组件名称。
但是你怎么知道哪些应该是组件?当你决定创建一个新函数或对象(创建组件的2种方式:函数和对象)时,就参考下面这些相同的技巧。技巧之一是:单一职责原则,这就是,一个组件原则上制作一件事。如果它逐渐扩展了,就应该被拆分为更细的组件。
由于我们经常展示一个JSON数据模型给用户,你将发现:如果你的模型被正确地建立,你的UI(也就是组件结构)也将很好的映射。这是因为UI和数据模型倾向于支持同样的信息架构,这意味着拆分UI为组件的工作总是琐碎的。我们要做的就是:将UI拆分成准确代表部分数据模型的一个个组件。

这里你将看到,在我们简单的应用中有5个组件,我们用斜体表示代表组件的数据。
1.FilterableProductTable(橙色):包含整个示例
2.SearchBar(蓝色):接收所有的用户输入
3.ProductTable(绿色):基于用户输入,展示和过滤数据集合
4.ProductCategoryRow(青绿色):展示每个分类头
5.ProductRow(红色):展示每个产品的详情
如果你在看 'ProductTable' 组件,你将发现表头(包含 'Name' 和 'Price' 标签)不是它自己的组件。这是一个偏好问题,关于是否这样使用存在争论。针对这个例子,我们留下它作为 'ProductTable' 组件的一部分,因为它是渲染 'data' 集合的一部分,是 'ProductTable' 组件的职责(之前说过:组件单一职责原则)。然而,如果这个表头变的复杂(例如:如果我们增加了字段排序功能),制作一个 'ProductTableHeader' 组件可能更有意义。
现在,我们已经在模拟环境中确定了我们的组件,让我们将它们排列成一个层级结构:
		FilterableProductTable
			SearchBar
			ProductTable
				ProductCategoryRow
				ProductRow

Step 2: Build A Static Version in React - 第二步:在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' 是从父传递给子数据的一种方式。如果你熟悉 'state' 概念,构建静态版本时,千万不要使用 'state'!'state' 仅用于交互,也就是,数据随着时间改变。由于这是一个静态版本,你不需要 'state'。
你可以自上而下,或自下而上构建静态版本。就是说,你可以从层级结构的顶层开始构建组件(FilterableProductTable),也可以从层级结构的底层开始构建组件(ProductRow)。在更简单的例子中,通常自上而下建立更简单,在大的项目中,自下而上建立则更简单一点。
在这个步骤的最后,你将有一个可重用的组件库,用来渲染你的数据模型。这些组件仅有 'render()' 方法,因为这是你应用的一个静态版本。在层级结构顶层的组件(FilterableProductTable)将你的数据模型,定义为 'props' 属性。如果你修改了数据模型,并再次调用了 'ReactDOM.render()',UI将被更新。查看UI如何更新,以及哪些地方进行了修改,是很简单的。因为没有什么复杂的东西。React的 'one-way data flow'(也称作 'one-way binding'),使得一切都模块化,并且运行的最快。
一个小插曲:
在React中,有2种数据模型:'props' 和 'state'。理解2者的不同很关键;如果不确定2者的不同,可查看之前文档中的解释。

Step 3: Identify The Minimal (but complete) Representation Of UI State - 第三步:确定表示UI状态的最小(但是完整的)集合
为了使你的用户界面交互,你需要能够触发对你的底层数据模型的修改。React使用 'state' 让这个变的很容易。
为了正确构建应用程序,首先你需要考虑应用需要的可变的 'state' 状态的最小集合。关键是 'DRY':Don't Repeat Yourself(不要重复)。找出你应用所需的状态的绝对的最小集合,并计算所有你需要的东西。例如,你正在构建一个 'TODO' 列表,那就仅定义一个 'TODO' 列表条目的数组;不要为计数定义一个单独的 'state' 变量。替代的,当你想渲染 'TODO' 个数时,直接使用 'TODO' 数组的长度。
考虑我们示例应用的所有数据。我们有:
1.产品的原始列表
2.用户已经输入的搜索内容
3.多选框的值
4.产品的筛选列表
让我们依次讨论,找出哪个应该被定义为 'state'。简单地问3个问题,关于每个数据:
1.它是否通过父组件的 'props' 传递?如果是,它可能不应该被定义为 'state'。
2.它是否随着时间保持不变?如果是,它可能不应该被定义为 'state'。
3.在你的组件中,它能不能基于其他 'state' 或 'props' 来计算?如果是,它可能不应该被定义为 'state'。
产品的原始列表,作为一个 'props' 被传入,因此不是 'state'。搜索内容和多选框,貌似可被定义为 'state',因为它们随着时间改变,并且不能通过其他一切来计算。最后,产品的筛选列表不是 'state',因为:它可以通过原始的产品列表和搜索文本或多选框的值,来计算。
因此最终,我们的 'state' 是:
1.用户已经输入的搜索内容
2.多选框的值


Step 4: Identify Where Your State Should Live - 第四步:识别 'state' 应该位于哪里
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')
);


我们已经定义了应用 'state' 的最小集合。接下来,我们需要定义,哪些组件使这些状态改变(哪些组件拥有这些 'state',也就是 'state' 定义在哪个组件内)。
记住:React中,数据是随着组件层级结构自上而下的单向数据流。可能不会马上明确哪个组件应该拥有哪些 'state'。这往往是新手理解中最具挑战性的一部分,因此通过下面的步骤来弄明白它:
1.对于应用程序的每个 'state':
1>识别基于 'state' 的每个组件(哪些组件需要state)
2>寻找一个拥有这些 'state' 的公共组件(层级结构中,所有需要 'state' 的组件的一个公共父组件)
3>公共组件或在层级结构上,更高层次上的另一个组件,应该拥有 'state'
4>如果你不能找到一个组件,拥有这个 'state',创建一个新组件仅仅为了拥有这个 'state',并且将这个新组件添加到公共组件的上层。
2.让我们针对我们的应用来使用这个策略:
1>'ProductTable' 需要基于 'state' 来过滤产品列表,'SearchBar' 需要基于 'state' 来展示 '搜索文本state' 和 '多选框state' 
2>拥有这些 'state' 的公共组件是 'FilterableProductTable'
3>概念上,'过滤文本' 和 '多选框值' 这2个 'state' 位于 'FilterableProductTable' 组件上,是有意义的。
我们已经决定我们的state位于 'FilterableProductTable' 组件。首先,添加一个属性:this.state = {filterText: '', inStockOnly: false} 到 'FilterableProductTable' 的 'constructor' 方法,表示应用的初始化 'state'。接着,传递 'filterText' 和 'inStockOnly' 给 'ProductTable' 和 'SearchBar' 组件,作为它们的属性。最后,使用这些属性,在 'ProductTable' 来过滤行,以及在 'SearchBar' 中设置表单字段的值。
你可以看到你的应用将如何工作:设置 'filterText' 为 'ball',并刷新你的应用。你将看到数据表正确的更新了。


Step 5: Add Inverse Data Flow - 第五步:添加一个反向数据流
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.refs.filterTextInput.value,
      this.refs.inStockOnlyInput.checked
    );
  }
  
  render() {
    return (
      <form>
        <input
          type="text"
          placeholder="Search..."
          value={this.props.filterText}
          ref="filterTextInput"
          onChange={this.handleChange}
        />
        <p>
          <input
            type="checkbox"
            checked={this.props.inStockOnly}
            ref="inStockOnlyInput"
            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')
);

到目前为止,我们已经创建了一个应用,。现在是时候以另一种方式支持数据流:在层级结构中的深层表单组件,需要在 'FilterableProductTable' 中更新 'state'。
React的数据流很明确,使得理解你的应用程序是如何工作的变的很容易,但相比于传统的双向数据绑定,它确实需要输入更多。
在示例中的当前版本中,如果你尝试输入或者勾选多选框,你将看到React会忽略你的输入。这是故意的,因为我们输入的值,总是等于从 'FilterableProductTable' 传入的 'state' 值。
让我们思考我们想要发生什么。我们想要确保:每当用户更改了表单,我们更新 'state' 来反映用户输入。因为组件仅应该更新它们自己的 'state', 'FilterableProductTable' 将传递一个回调函数给 'SearchBarla',来触发 'state' 更新。我们可以在表单元素上,使用 'onChange' 事件来通知它。通过 'FilterableProductTable' 传递的回调,将调用 'setState()' 方法,应用将被更新。
虽然这听起来很复杂,但它确实只是几行代码。而且贯穿整个应用中,你的数据是如何流动,是很明确的。


And That's It - 这就是它
希望,通过这篇教程让你知道,使用React如何来创建组件和应用。虽然相比于你之前使用过的其他类库,React可能需要写更多,但是记住:代码的阅读远比书写更重要,并且阅读通过React创建的模块化、清晰的代码是极其容易的。当你开始建立大型的组件库时,你将会欣赏这个明确化和模块化,重用性代码,你的代码行将减少!
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值