React的Nested Component模式

leverli 7年前發布 | 7K 次閱讀 React 前端技術

JSX dot notation

一個偶然的機會,發現React的JSX語法里,Component Type是可以寫成這樣的:

<this.FlatButton />

React/JSX的Component Type是支持dot notation的,主要是為了方便把一組Component裝在一個Object容器里,這樣在export/import的時候很方便;如果一個Component是匿名的或者名字是小寫字母開頭的,JSX并不接受,但可以用一個大寫字母開始的變量名來轉換一下。

在React的官方文檔中給出來的使用dot notation的例子是:

<MyComponents.DatePicker color="blue" />

它給人一個錯覺,似乎容器的名字也必須是大寫字母開始的,但簡單試一下就知道并非如此,dot notation前面的容器名字沒有任何限制。例如

import { FlatButton } from 'material-ui'

const wrap = { MyButton: FlatButton }

// in render <wrap.MyButton label='hello' /></code></pre>

是完全可用的。Anyway,我們澄清了一個細節,JSX支持dot notation,這一點沒問題,dot前面的容器對象名稱無限制,所以 <this.FlatButton /> 可以工作。

Nested Component

那么,為什么要這樣用呢?

我舉一個例子。比如在綁定行為時我們經常有這樣的寫法:

class Foo extends React.Component {

handle() { // ... }

render () { <FlatButton onTouchTap={this.handle.bind(this)} /> } }</code></pre>

如果不想總是寫 bind(this) ,我們可以換一種方式寫:

class Foo extends React.Component {

constructor(props) { super(props)

this.handle = () => {
  // ...
}

}

render () { <FlatButton onTouchTap={this.handle} /> } }</code></pre>

把 handle 從類方法中搬到構造函數里去定義成為arrow function,雖然arrow function不能bind this,但是在函數內寫this仍然是有效的,因為constructor里有this。

上面的例子里沒有參數傳遞,如果有參數傳遞,后者的寫法很少會犯錯誤,但前者有時候忘了bind,或者搞錯了參數形式都容易出問題。

Function Component

同樣的,如果我們把component用類似的方式定義在constructor內,如果這是一個function component,它可以直接訪問容器內的 this ,當然也就能直接訪問容器內的 this.state 和 this.props ,這樣直接的好處就是可以不用props翻譯父組件的state或者props傳遞給子組件。

如果子組件對應多個數據對象實例,那么只要把這個數據對象本身作為props傳遞給子組件即可,例如:

class Foo extends React.Component {

constructor(props) { super() this.state = { selected: [] }

this.deleteItem = item => {
  //...
}

this.Bar = props => {
  let item = props.item
  return (
    <div>
      {item.name} 
      <FlatButton label='delete' 
        onTouchTap={() => this.deleteItem(item)}
      />
    </div>
  )
}

}

render() { <div> { this.props.items.map(item => <this.Bar item={item} />) } </div> } }</code></pre>

這樣寫的 Bar Component,直接在父組件的構造函數內,它當然可以隨意訪問父容器的 state 和 props ;

但好處不限于此。

在實際的場景中,常常出現因為 item 的數據對象是多態的,我們可能需要定義很多種 Bar 來實現不同的顯示和行為,在這種情況下,各種 Bar 的實現里,不論是行為還是表示,都有很多共用的地方。但是React的Component并不能使用繼承的方式來實現共性;所以實際的情況是:

  1. 對于表示,如果 MyFirstBar 和 MySecondBar 之間需要共用,那么仍然需要抽取Component。

  2. 對于行為,寫在父組件里,向子組件binding。

但是如果寫成上述的形式,抽取共用的部分仍然可以寫成 this.BarCommonPart 這樣的形式,同樣的,無須傳遞 props ,抽取共同行為的部分就更加簡單了,在子組件之內直接調用父組件方法即可,不需要用 onSomethingHappened 之類的 props 傳遞。

Class Component

當然上面寫的都是Function Component,可以定義為arrow function,寫在父組件的構造函數里,共享父組件的 this ,那么如果子組件需要有態呢?需要是Class Component呢?

同樣可以。

雖然我們可能很少在實踐中寫出匿名class,但是在JavaScript里它是合法的。上面的 Bar 如果是Class Component,結果是這樣:

class Foo extends React.Component {

constructor(props) { super() const that = this

this.state = {
  selected: []
}

this.deleteItem = item => {
  //...
}

this.Bar = class extends React.Component {

  constructor(props) {
    super(props)
    this.state = { open: false }
  }

  render() {
    let item = this.props.item
    return (
      <div>
        {item.name} 
        <FlatButton label='delete'
          onTouchTap={() => that.deleteItem(item)}
        />
      </div>
    )
  }
}

}

render() { <div> { this.props.items.map(item => <this.Bar item={item} />) } </div> } }</code></pre>

寫成這樣之后,在Bar里面的 this 不再指向父組件了,而是指向了子組件自己;但是我們可以在父組件容器里定義一個 that ,作為閉包或者叫詞法域(lexical scope)變量,在整個 Bar 的內部這個 that 都是可用的。

這樣無論是Function Component還是Class Component都可以nest在父組件中,不僅可以直接訪問父組件的全部上下文,更可以方便共享表示和行為,直接在子組件的方法內調用 this.setState() 或者 that.setState() 更新父組件的行為也完全不是問題。

And More

還不僅如此;

父組件作為上下文還有其他功效,例如:

class Foo extends React.Component {
  constructor(props) {
    super()
    const that = this

this.colors = {
  primary: () => '#FF89E0',
  secondary: () => '#DD7633',
  // ...
}

this.dims = {
  tableHeaderHeight: () => 64,
  tabelDataHeight: () => 48,
  // ...
}

this.styles = {
  mainText: () => ({
    fontSize: 14,
    fontWeight: this.state.editing ? 'normal' : 'bold',
  })
  // ...
}

} }</code></pre>

你可以看出父組件完全可以自己作為一個上下文的小世界,定義統一的color, dimension和style體系;他們都在父組件的構造函數內,因此可以在此訪問所有狀態,如果需要在這個組件內做動態,這非常方便。

Summary

如果你理解JavaScript的class和閉包是高度相似的(把function scope當成對象來理解),你就理解這個Pattern的要義:把React.Component從class對象翻成了類似閉包的基于lexical scope的context工作的方式。

既然React.Component不能基于class繼承實現重用,那么為什么不這么做讓書寫代碼變得容易呢?在這個context內,你連額外的狀態管理器(例如redux)都不需要,因為一切都是全局的,在任何地方都可以調用父組件的 setState 方法,而結果就是所有子組件都可以體現變化。

我在過去的兩天里把一個大約2000-3000行代碼的單頁面寫成了這種形式,目前感覺非常好,不再有奇怪的不容易覺察的行為binding,也扔掉了所有的 props/state 傳遞,也不需要什么額外的東西來管理狀態。

當然這種做法反模式的地方是,這樣寫在容器內的組件在外部無法重用了,是的,如果需要外部重用我們仍然要回到寫獨立的React組件的模式,但是對于實際應用中,很多復雜組件都有自己的獨特性,而容器拆解不可避免,所以至少在不太需要外部重用的地方,這種Nested Component Pattern,是一種不但可行,而且非常簡潔易用的方式。

 

來自:https://segmentfault.com/a/1190000008014225

 

 本文由用戶 leverli 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!