用React的方式思考

作者:Pete Hunt

翻译:孙和

原文链接

构建大型、反应迅捷的web app,我首选react。我们在facebook和instagram中都用了它,扩展的不错。

当你构建APP时,React重塑了你的思考方式。我会在这篇文章中展现,用react构建应用的思考过程。

首先,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"}
];

第一步:把UI分解成组件树
你要做的第一件事就是,在设计图上,把组件和子组件用线框出来,还要给他们命名。

thinking-in-react-components.png

如何划分组件呢?方法同划分函数或者对象一样。你要遵循单一功能原则,就是说,一个组件只做一件事。如果组件不断膨胀,就要把它再拆解成更小的子组件。

如果你的数据模型构建的好,那么你的UI和组件结构就会相应的更清晰明了。这是因为UI和数据模型遵循同样的信息架构,这意味着如果数据模型足够清晰,那么把UI分解成组件就是小事一桩。只需要把它拆分成组件,每个组件代表数据模型的一部分。

在这个APP中,我们有5个组件:
1、FilterableProductTable:包含整个APP
2、SearchBar:接收用户输入
3、ProductTable:根据用户输入,显示和筛选数据集合
4、ProductCategoryRow:显示每一个分类的标题
5、ProductRow:每一个产品,显示一行

第二步:静态版本

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:'$49.99',stocked:true,name:'Baseball'},
{category:'Sporting Goods',price:'$49.99',stocked:false,name:'Basketball'},
{category:'Electronics',price:'$49.99',stocked:true,name:'iPod Touch'},
{category:'Electronics',price:'$49.99',stocked:false,name:'iPhone 5'},
{category:'Electronics',price:'$49.99',stocked:true,name:'Nexus 7'}
];
ReactDOM.render(
   <FilterableProductTable products={PRODUCTS} />,
    document.getElementById('container')
);

构建一个可以渲染数据模型的静态APP,你要构建很多组件。这些组件重用了其他组件,并且用props传递数据。可以利用props,从父组件向子组件传递数据。在构建静态版时,一定不能用state。state只用于交互,就是说,表示不断变化的数据。

你可以自底向上或者自顶向下构建APP,如果自底向上,你先构建ProductRow;如果自顶向下,你先构建FilterableProductTable。简单情形下,一般自顶向下。大型项目中,一般自底向上,并在构建过程中,编写测试用例。

完成这一步,你会有一些可重用的组件,这些组件渲染你的数据模型。这些组件只有render方法,因为你的APP是静态的。组件树顶端的FilterableProductTable将你的数据模型作为prop。如果你改变数据模型,重新调用ReacDOM.render()方法,UI会更新。React的单向数据流让一切模块化、迅捷。

第三步:找出UI state的最小完备集
为了给UI添加交互,你需要触发数据模型的变化。React用state轻松实现了这一点。
首先,你要想好可变状态的最小集合。关键是DRY,不要重复自己。找出状态的最小集合,用这个集合计算其他需要的状态。例如,你构建一个TODO List,只要保留TODO items 数组,不要为计数保留一个额外的状态变量。当你需要计数时,只需计算TODO items 数组的长度。

考虑我们所有的数据,我们有:原始产品列表,用户键入的搜索文字,checkbox 的值,筛选过的产品列表。

为了确定哪一个是state,只要问三个问题:
1、它是否通过props从父组件传入?如果是,可能不是state
2、它不断变化吗?如果不是,那么可能不是state
3、它可以用其他state或者props计算出来吗?如果是,那么不是state

原始产品列表,作为props传入,不是state;
搜索文本和checkbox是state,因为他们不断变化,并且不能通过其他东西计算出来;
筛选后的产品列表不是state,因为它可以通过原始产品列表和搜索文字或checkbox的取值计算出来。

最终,state是:
用户输入的搜索文本
checkbox的值

第四步:确定state的位置

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({
  render:function(){
    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>
    )
  }
})
var FilterableProductTable=React.createClass({
  getInitialState:function(){
    return{
      filterText:'',
      inStockOnly:false
    }
  },
  render:function(){
    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。
记住:react的核心是沿着组件树流动的单向数据。弄清楚哪一个组件拥有哪一个state,是最有挑战的部分。
对于每一个state:
确定每一个根据这个state渲染的组件;
找到总组件(组件树中,所有需要这个state的组件,上面的那个组件);
要么是总组件,要么是组件树中更高层的组件,拥有这个state;
如果你找不到可以合理拥有这个state的组件,创建一个新的组件,这个组件只是为了持有这个state,并把这个组件加到组件树中,在总组件之上

根据这一策略:
ProductTable需要根据state筛选产品列表,SearchBar需要展示搜索文本和选中状态;
总组件是FilterableProductTable;
筛选文本和选择值放在FilterableProductTable组件中很合适

棒!我们已经决定把state放在FilterableProductTable中。首先,在FilterableProductTable中添加getInitialState()方法,返回反映初始状态的对象{filterText:'',inStockOnly:false}。然后,把filterText和inStockOnly作为prop传到ProductTable和SearchBar中。最后,用这些props在ProductTable中筛选rows,在SearchBar中设置表单域的值。

第五步:添加逆向数据流

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

目前,我们已经构建了一个可以正确渲染的APP,在渲染过程中,props和state函数沿着组件树自上而下流动。现在,我们要支持数据反向流动:组件树底层的form组件更新组件树顶层的FilterableProductTable组件state。

React 让这一数据流显式表现出来,这样,在理解程序如何运转时会更容易。但是,相对于传统的双向数据绑定,你需要书写更多。React提供了一个叫ReactLink的插件,让这种模式像双向数据绑定一样方便,但在本文中,我采用显式的方式。

在当前版本中,如果你键入或者勾选,不会有任何反应。因为我们已经将input中的value prop和从FilterableProductTable中传入的state设置为相等。

我们希望当用户改变表单时,我们能更新state来反应用户的输入。由于组件只能更新自己的state,FilterableProductTable将会传递一个回调函数给SearchBar,每当state更新时,这个回调函数就会触发。我们可以用inputs上的onChange事件,监听这一变化。每当onChange事件被触发,由FilterableProductTable传入的回调函数会调用setState()方法,app就会更新。

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,763评论 25 707
  • React的思想 在我看来, React 是较早使用 JavaScript 构建大型、快速的 Web 应用程序的技...
    KavinZhou阅读 583评论 0 4
  • 不知生活中是不是常有人夸你懂事,也不知道你听后是什么感受? 我不承认自己有多好,却是大部分人眼中懂事的那个好孩子。...
    语墨北北阅读 520评论 0 0
  • 又到六月了,坐在车上会听到同事讨论自家孩子该从幼儿园宝宝变成一年级小学生的事情,语言里充满着一些焦虑与不确定,他们...
    飞飞来啦阅读 553评论 0 0
  • 每个人在遇到暴击的时候,第一反应肯定不会以感激的态度来对待。那么感激这个行为,什么时候才会出现呢? 我认为一般会有...
    聪明大白阅读 6,434评论 14 5