React的Nested Component模式
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并不能使用繼承的方式來實現共性;所以實際的情況是:
-
對于表示,如果 MyFirstBar 和 MySecondBar 之間需要共用,那么仍然需要抽取Component。
-
對于行為,寫在父組件里,向子組件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