React式思维

在我们看来,React是用JavaScript构建大型、敏捷Web应用的首要方法。它在Facebook和Instagram上为我们提供了很好的扩展。
React中一个重要的部分就是在构建应用时的思考方式。在这篇文档中,我们将使用React构建一个可搜索的产品数据表,并带你一起经历整个思考过程。

从设计图开始

想象一下,我们已经有了一个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"}
];

步骤1:将UI分成组件层级

你想做的第一件事也许是在设计图上为每个组件(包括子组件)画上盒子,并给他们命名。如果你和一个设计师一起在做这件事,他们或许已经完成了这一步,所以直接去找他们要就好了。他们的Photoshop图层的名字或许最终成为你React组件的名字!
但如何划分组件?只需要使用决定创建新函数和对象的方法就好。该方法是单一责任原则,即一个组件理想情况下只做一件事。如果组件规模最终增长,也许就该分解成更小的子组件。
由于会经常向用户显示JSON数据模型,你会发现如果你的模型构建正确,UI(以组件结构)将会很好的映射。这是因为UI和数据模型通常遵循相同的信息结构,这意味着将UI分成组件的工作往往很简单。只需要将组件分解为代表数据模型一部分即可。

在这个简答的应用中有五个组件。我们将用斜体标出每个组件对应的数据。

  1. FilterableProductTable(橙色):包含了整个例子
  2. SearchBar(蓝色):接受所有用户输入
  3. ProductTable(绿色):根据用户的输入显示和筛选数据集合
  4. ProductCategoryRow(绿松色):显示每个分类标题
  5. ProductRow(红色):显示每个product

看看ProductTable,你会发现表格头(包含"Name"和"Price"标签)并属于组件。这是喜好问题,两种方式都有争论。在这个例子中,我们将它作为ProductTable的一部分,因为它是渲染数据集合的一部分,而这是ProductTable的职责。不过,如果这个表格头变得非常复杂(比如,如果我们使其可排序),就可以单独将其作为ProductTableHeader组件。
现在我们已经在设计图中标记出组件了,让我们将他们放进层级。这很简单。设计图中,出现在另一个组件中的组件,在层级中应作为孩子:

  • FilterableProductTable
    • SearchBar
    • ProductTable
      • ProductCategoryRow
      • ProductRow

步骤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')
);

在CodePen上试一试

现在有了组件层级,是时候完成你的应用了。最简单的方法就是使用数据模型渲染一个没有交互的UI。最好将这两个过程分开,因为构建静态版本需要大量的编码、无需思考,而增加交互就需要大量的思考而无需大量的编码。下面我们来看下原因。

要构建渲染数据模型的静态版本应用,需要构建可重用组件,并使用props传递数据。通过props将数据从父传向子。在构建静态版本中万不可使用状态。状态仅用于交互性,状态是随时间变化的数据。所以应用的静态版本中不需要它。

构建可以自上而下,或自下而上。也就是说,你既可以从层次中的高层组件开始构建(也就是从FilterableProductTable开始),也可以从底层开始(ProductRow)。在简单些的例子中,一般自上而下更容易一些,而大型的项目中,自下而上会更简单些,也更容易书写测试。

在此步结束时,你将拥有一个用于呈现数据模型的可重用组件库。因为这是应用的静态版本,所以组件只有render()方法。层次上层的组件(FilterableProductTable)将数据模型作为prop。如果你对底层数据模型进行更改,并再次调用ReactDOM.render(),UI将会得到更新。因为没发生什么复杂的事情,很容易就能看到UI何如更新,哪里发生了改变。React的单向数据流(也叫单向绑定)让一切模块化、快速化。
如果你在执行这步时需要帮助,请参阅React文档

简短的插曲:Props和状态

在React中有两种“模型”数据:props和状态。了解两者之间的区别很重要;如果你不清楚有什么区别,请浏览官方React文档

步骤3: 确定UI状态的最小表示(但完整)

为了让你的UI可交互,你需要能够触发对底层数据模型的变更。React通过状态,让这变的很简单。

为了正确的构建应用,首先需要想想应用需要的最小可变状态集合。关键就在于DRT(Don't Repeat Yourself)。找出应用所需状态的绝对最小表示,并计算其他所有的需求。比如:如果在构建一个TODO列表,只需要保留TODO项数组;不用单独保存一个状态变量来表示数量。如果你要渲染TODO的数量,简单的获取TODO项数组的长度即可。

想想示例应用中所有的数据。有:

  • 初始的产品列表
  • 用户输入的搜索文本
  • 复选框的值
  • 筛选后的产品列表

让我们一个个看过去,并找出哪个是状态。对每个数据提出三个问题即可:

  1. 是否由父组件通过props传入的?如果是的话,他可能不是状态。
  2. 是否保持不变?如果是的话,他可能不是状态。
  3. 是否可通过组件中其他的状态或props计算得到?如果是的话,他不是状态。

初始的产品列表作为props传入,所以他不是状态。搜索文本和复选框看起来是状态,因为他们会发生变化,且不能由其他值计算得到。最后一个,筛选后的产品列表不是状态,因为它可以通过组合初始产品列表,搜索列表以及复选框的值计算得到。

因此,我们的状态有:

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

步骤4:确定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')
);

在CodePen上试一试

既然我们已经确定了应用状态的最小集合。接下来我们应该找出哪个组件拥有这个状态。

记住:React就是组件层次中的单向数据流。搞清楚组件拥有什么状态,对于新手来说是最具挑战性的部分,可以按以下步骤来做:

遍历应用中的每个状态:

  • 确认出需要该状态进行渲染的所有组件
  • 找出公共所有者组件(层次中一个在所有需要该状态组件上方的组件)
  • 公共组件或层次更靠上的组件应该拥有该状态
  • 如果不存在这样一个组件,则创建一个只用于保存改状态的新组件,然后将其插入公共所有者组件的层次结构上方。

在我们的应用中执行一遍该策略:

  • ProductTable需要根据状态筛选产品列表,SearchBar需要显示状态:搜索文本和复选值。
  • FilterableProductTable是公共所有者组件。
  • 筛选文本和复选值应该放在FilterableProductTable

因此我们决定将状态放在FilterableProductTable中。首先在FilterableProductTableconstructor中添加一个实例属性this.state = {filterText: '', inStockOnly: false}来表示应用的初始状态。然后将filterTextinStockOnly作为prop传递给ProductTableSearchBar。最后使用这些props来筛选ProductTable中的行,设置SearchBar中表单项的值。

你可以将筛选文本设置为"ball",并刷新,来看看应用会有怎样的表现。你将看到数据表格得到了正确的更新。

步骤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;
    console.log(this.props.inStockOnly)
    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.handleFilterTextInputChange = this.handleFilterTextInputChange.bind(this);
    this.handleInStockInputChange = this.handleInStockInputChange.bind(this);
  }
  
  handleFilterTextInputChange(e) {
    this.props.onFilterTextInput(e.target.value);
  }
  
  handleInStockInputChange(e) {
    this.props.onInStockInput(e.target.checked);
  }
  
  render() {
    return (
      <form>
        <input
          type="text"
          placeholder="Search..."
          value={this.props.filterText}
          onChange={this.handleFilterTextInputChange}
        />
        <p>
          <input
            type="checkbox"
            checked={this.props.inStockOnly}
            onChange={this.handleInStockInputChange}
          />
          {' '}
          Only show products in stock
        </p>
      </form>
    );
  }
}

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

  handleFilterTextInput(filterText) {
    this.setState({
      filterText: filterText
    });
  }
  
  handleInStockInput(inStockOnly) {
    this.setState({
      inStockOnly: inStockOnly
    })
  }

  render() {
    return (
      <div>
        <SearchBar
          filterText={this.state.filterText}
          inStockOnly={this.state.inStockOnly}
          onFilterTextInput={this.handleFilterTextInput}
          onInStockInput={this.handleInStockInput}
        />
        <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')
);

在CodePen上试一试

到目前为止,我们已经构建了一个应用程序,渲染正确且pros和状态在层次中自上而下。现在是时候支持数据以另一种方式流动了:深层次中的表单组件需要更新FilterableProductTable中的状态。

React通过明确的数据流,使得我们很容易了解程序的工作原理,但它比起传统的双向数据绑定需要更多的编码。

在当前版本的例子中,如果你输入或点击复选框,将看到React会忽略你的输入。这是故意的,因为我们已经将inputvalueprop设置始终等于由FilterableProductTable传入的state了。

我们希望当用户更改表单时,状态都会根据用户的输入得到更新。由于组件只能更新自己的状态,FilterableProductTabel将更新状态的回调函数传给SearchBar。我们可以使用onChange事件来监听input。FilterableProductTable传入的回调函数将调用setState(),来更新应用。

虽然这听起来很复杂,但它只需要几行代码。而且整个应用中的数据流通也非常明确。

就这样吧

希望这些足够让你了解如何使用React构建组件和应用。相比之前,这可能需要更多的编码,但记住代码更多的是读大于写,而读这种模块化、明确的代码是非常容易的。当开始构建大型组件库的时候,你将感激这种显式和模块化,并通过代码重用,你代码的行数将开始缩小。:-)

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

推荐阅读更多精彩内容