[译]Thinking in React

编者按

使用React的思想来构建应用对我在实际项目中以及帮助他人解决实际问题时起到了很大作用,所以我翻译此文来向那些正在或即将陷入React或React-Native深坑的同胞们表示慰问。网上已经有人翻译过,我想用更易读的语言翻译一次,这也是我首次如此一本正经的翻译技术文章给大众阅读,权当练习吧。
原文地址:https://facebook.github.io/react/docs/thinking-in-react.html
转载还请注明出处以及原文地址,出于对作者和译者劳动成果的尊重吧,谢谢了我的哥。

Thinking in React

作者:Pete Hunt 译者:Rex Rao (sohobloo)

我认为React是使用JavaScript构建高性能大型Web应用的首选方案,我们已经在Facebook和Instagram中广泛使用,哎哟,效果不错哟。
React的众多优势之一是——且看它如何让你能顺着思路构建应用。在此,我将引领你用React逐步构建出一个可搜索的商品列表应用。

从模型图开始

假设设计师已经为我们提供了API并可以返回模拟的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"}];

第一步:拆分并构建界面的组件层次结构树

你应该做的第一件事是为你原型图所有组件和子组件画个边框、起个名。要是你跟设计师坐一起,找他们喝喝茶,说不定他们的Photoshop图层名恰巧可以用作你React组件的名字!(译者:我只能说,Too young too simple, sometimes naive!)
但你怎么知道如何拆分一个组件呢?这和你平时决定是不是要新建一个函数或者类的道理一样一样的。其中有个叫做单一职责原则的原理,也就是说理想状态下一个组件只做一件事,当他需要做更多,那就应该继续拆拆拆。
如果你经常向用户展示JSON数据,你会发现只要你的数据模型建得好,你的界面乃至你的组件架构也会完美的与之映射。因为界面和数据模型倾向于支持相同的信息架构,这让界面拆分工作变简单了,拆分出的一个组件只对应展示数据模型中的一种数据就行。


你看,咱这简单的应用有5种组件。我用斜体标示出了每个组件要展示的数据。

  1. FilterableProductTable(橙色): 包含整个示例
  2. SearchBar(蓝色): 接收用户输入(user input)
  3. ProductTable(绿色): 显示基于用户输入(user input)过滤的数据集 (data collection)
  4. ProductCategoryRow(青色): 显示分类( category)
  5. ProductRow(红色): 显示每一行商品(product)

看ProductTable你会发现表头(含"Name"和"Price"标签)并没有拆分成组件,这是出于一种存在争议的个人喜好而已啦。这个例子中,既然渲染数据集 (data collection)是ProductTable的职责,那就让它作为此组件的一部分好了。要是它再复杂一点的话(比如排序功能),那就另当别论独立成ProductTableHeader组件咯。
让我们把从原型图中定义的组件组合成层次结构树。如果一个组件出现在另一个组件中,那么这个组件就是它的子组件,so easy:

  • FilterableProductTable
  • SearchBar
  • ProductTable
    • ProductCategoryRow
    • ProductRow

第二步:用React做个静态版

var ProductCategoryRow = React.createClass({
  render: function() {
    return (<tr><th colSpan="2">{this.props.category}</th></tr>);
  }
});

var ProductRow = React.createClass({
  render: function() {
    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>
    );
  }
});

var ProductTable = React.createClass({
  render: function() {
    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>
    );
  }
});

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

var FilterableProductTable = React.createClass({
  render: function() {
    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')
);

有了组件层次结构,是时候表演真正的技术了实现你的应用了。最简单的方式是把数据渲染到界面上,但是不带交互功能。最好是分离这些步骤,因为构建一个静态版本更多是需要你敲键盘而增加交互功能就需要你敲脑袋了。
将你的应用构建出一个静态版本来展示数据模型,你也许会需要构建组件来复用其他组件,用属性(props)传入数据。 属性(props)是一种将数据从父组件传入子组件的途径。 即便你对状态(state)模式非常熟悉,在静态版本中也不要使用状态哦。状态是留给交互来处理那些会变化的数据使用的。作为一个静态版请无视之。
你可以用自上而下或自下而上的方式构建应用。既可以从最顶层组件开始(比如从FilterableProductTable开始)也可以从最底层组件开始(如ProductRow)。在简单的示例中,自上而下往往更容易;而在大型项目中,使用自下而上更好,你还能方便的写单元测试呢!
这一步完成之后,你就有了一个可以展示你的数据模型的组件库。作为一个静态版本,每个组件都只有一个 render()方法。顶层组件(FilterableProductTable)通过属性(prop)获得你的数据模型。如果此时你改变你的基础数据模型并再次调用ReactDOM.render(),界面会刷新。界面的刷新和变化了一目了然直接了当。React的单向数据流(又名单向绑定)让所有事情有序且快速。

如果在这一步中遇到问题,你可以参考React文档

小插曲儿: 属性(props)与状态(state)

React中有两类数据模型:属性和状态,了解他们的区别是很有必要的!还不太清楚?来来来看这里React官方文档咋说的。

第三步:定义最小(完整)的界面状态值

界面想要动起来?数据必须变起来!React使用状态(state)来实现。
若想正确构建你的应用,首先你得考虑你的应用至少需要一组什么样的可变状态值。来跟我念口诀:取其精华,去其糟粕。找出你的应用的那组干货——绝对最小化的界面状态值组,并且其他任何需要都可以通这组值计算得出。比如你要构建一个待办列表,只需要维护一组待办项即可;你不需要再维护这组列表的个数的值,因为在你需要展示待办数时可以直接获取列表长度得到结果。
来看看我们例子里有哪些数据:

  • 原始的商品列表
  • 用户输入的搜索文本
  • 勾选框的值
  • 筛选后的商品列表

让我们逐条看看哪些是状态,对每一条数据三省吾身:

  1. 是否是父组件传入的属性?如果是的,估计不是状态。
  2. 是否会随时间变化改变?如果不会变,估计不是状态。
  3. 能否从其他状态或属性计算得到?如果可以,肯定不是状态。

原始商品列表通过属性传进来,因此它不是状态。搜索框的值和勾选框的值可以改变而且其他东西也计算不出来这些值,看上去应该是状态。最后,筛选后的商品列表也不是状态,因为它可以通过原始商品列表、搜索框的值和勾选框的值计算得出。
最后得出我们需要的状态:

  • 用户输入的输入框的值
  • 勾选框的值

第四步:给状态找个家

最小状态集新鲜出炉,接下来我们需要定义哪些组件会变化,或者说拥有这些状态。
记住了: React数据总是单向且「下流」的——流向组件层次中的底层。可能并不是一开始就看得出哪个组件拥有什么状态。这常常是萌新最难理解的部分,所以就让老司机带带你吧:

  • 对于你应用的每一条状态:
  • 找出每一个需要基于此状态来渲染界面的组件。
  • 找到它们共同的爹(一个在组件层次中需要此状态的所有控件的顶层父组件)。
  • 它们共同的父组件或更高层级的组件都可以作为状态的持有者。
  • 如果你觉得哪个组件持有这个状态都很别扭,可以为了这个状态创造一个新的组件来持有,并把这个新组件加到它们共同父组件的上层结构中的任何合适位置。

针对我们的应用,让我们根据以上策略捋一捋:

  • ProductTable需要根据状态值来过滤商品列表,SearchBar需要显示搜索文本和勾选框状态值。
  • FilterableProductTable是它们的共同父组件。
  • 看起来搜索文本和勾选框值放在FilterableProductTable挺合适。

就这么愉快的决定了,把这些状态放FilterableProductTable里吧。
首先在FilterableProductTable中增加getInitialState()(译者:ES6中如果用class构建组件,初始化状态的方法将发生改变)方法并返回{filterText: '', inStockOnly: false}来对应应用的初始状态。然后将filterText和inStockOnly作为属性传给ProductTable和SearchBar
。最后就用属性来过滤ProductTable中的商品列表并把搜索文本设置到SearchBar的输入框中。

来看看你应用的表现如何:把filterText设置成"ball"然后刷新。厉害了我的哥,列表正确的更新了!

第五步:增加反向数据流

var ProductCategoryRow = React.createClass({
  render: function() {
    return (<tr><th colSpan="2">{this.props.category}</th></tr>);
  }
});

var ProductRow = React.createClass({
  render: function() {
    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>
    );
  }
});

var ProductTable = React.createClass({
  render: function() {
    var rows = [];
    var lastCategory = null;
    this.props.products.forEach(function(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;
    }.bind(this));
    return (
      <table>
        <thead>
          <tr>
            <th>Name</th>
            <th>Price</th>
          </tr>
        </thead>
        <tbody>{rows}</tbody>
      </table>
    );
  }
});

var SearchBar = React.createClass({
  handleChange: function() {
    this.props.onUserInput(
      this.refs.filterTextInput.value,
      this.refs.inStockOnlyInput.checked
    );
  },
  render: function() {
    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>
    );
  }
});

var FilterableProductTable = React.createClass({
  getInitialState: function() {
    return {
      filterText: '',
      inStockOnly: false
    };
  },

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

  render: function() {
    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里的状态。

React明确的数据传递能让你更容易搞清楚你的程序是怎么运作的,但比起传统的双向数据绑定你就需要敲稍微多一点的代码了。 React提供了一个叫ReactLink的插件来让这种模式变得和双向绑定一样方便,但本文的目的在于让一切明晰,暂不使用。

如果你尝试在当前版本的示例中输入或勾选,你会发现React完全无视你的输入。 怎么回事难道有Bug?乖乖我们故意的!因为我们刚才把input的value属性设置成总是等于FilterableProductTable传进来的状态了。

然并卵,我们需要用户的输入立刻更新状态。既然控件只允许更新自己的状态,FilterableProductTable可以传一个每次状态需要发生变化时都会触发的回调函数回传到SearchBar。我们可以用输入框的onChange事件来触发并在FilterableProductTable传入的回调函数中调用setState()来更新状态。

看上去好像很复杂的样子,其实只是多了几行代码而已,但这真真真的让你能看清数据是如何在你应用的身体里流来流去的。

没错就是这样

希望这篇文章能在你用React构建组件或应用时给你点亮一盏明灯。虽然可能比以前要搬更多砖,但请你记住代码写出来是要可以给人阅读的,特别是那些标准统一、逻辑清晰的代码更赏心悦目。当你开始构建大型的控件库的时候,你会感激这种规则化、清晰化的风格,再加上代码的复用,你的代码行数会得到缩减。☺

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,455评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,597评论 18 139
  • 原教程内容详见精益 React 学习指南,这只是我在学习过程中的一些阅读笔记,个人觉得该教程讲解深入浅出,比目前大...
    leonaxiong阅读 2,809评论 1 18
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,019评论 4 62
  • 2010.9.20 “郭哥,隔壁小区的正义帮的吴狗又来咱们小区偷砖了,他们还说要把我们堆的塔踢倒,怎么办?”,...
    就是一哈哈阅读 361评论 0 0