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' />
是完全可用的。Anyway,我们澄清了一个细节,JSX支持dot notation,这一点没问题,dot前面的容器对象名称无限制,所以<this.FlatButton />
可以工作。
Nested Component
那么,为什么要这样用呢?
我举一个例子。比如在绑定行为时我们经常有这样的写法:
class Foo extends React.Component { handle() { // ... } render () { <FlatButton onTouchTap={this.handle.bind(this)} /> } }
如果不想总是写bind(this)
,我们可以换一种方式写:
class Foo extends React.Component { constructor(props) { super(props) this.handle = () => { // ... } } render () { <FlatButton onTouchTap={this.handle} /> } }
把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> } }
这样写的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> } }
写成这样之后,在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',}) // ... } } }
你可以看出父组件完全可以自己作为一个上下文的小世界,定义统一的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,是一种不但可行,而且非常简洁易用的方式。