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.statethis.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,直接在父组件的构造函数内,它当然可以随意访问父容器的stateprops

但好处不限于此。

在实际的场景中,常常出现因为item的数据对象是多态的,我们可能需要定义很多种Bar来实现不同的显示和行为,在这种情况下,各种Bar的实现里,不论是行为还是表示,都有很多共用的地方。但是React的Component并不能使用继承的方式来实现共性;所以实际的情况是:

  1. 对于表示,如果MyFirstBarMySecondBar之间需要共用,那么仍然需要抽取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>
  }
}

写成这样之后,在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,是一种不但可行,而且非常简洁易用的方式。


uglee
1.1k 声望1.2k 粉丝