Advanced-react-patterns(2)

Zaynex發表於2019-02-09

文字是 ADVANCED-REACT-PATTERNS 第二篇。

Static 妙用

function App() {
  return (
    <Toggle
      onToggle={on => console.log(`toggle`, on)}
    >
      <Toggle.On>The button is on</Toggle.On>
      <Toggle.Off>The button is off</Toggle.Off>
      <Toggle.Button />
    </Toggle>
  )
}
複製程式碼

首先外層是一個 Toggle元件,包括著內部的子元件。因為外層元件包裹的命名比較清晰。

注意這中間有不太常見的元件使用方式。

<Toggle.On>
複製程式碼

Toggle元件是一個 component 類, On 是它的屬性,不過既然作為元件使用,說明也是一個 component 類。
繼續研究下 Toggle 元件的實現,去尋找答案。

class Toggle extends React.Component {
  static On = ToggleOn
  static Off = ToggleOff
  static Button = ToggleButton
  static defaultProps = {onToggle: () => {}}
  state = {on: false}
  toggle = () =>
    this.setState(
      ({on}) => ({on: !on}),
      () => this.props.onToggle(this.state.on),
    )
  render() {
    const children = React.Children.map(
      this.props.children,
      child =>
        React.cloneElement(child, {
          on: this.state.on,
          toggle: this.toggle,
        }),
    )
    return <div>{children}</div>
  }
}

複製程式碼

在實際 debug 的時候元件渲染民時最終還是被替換成 <ToggleOn> 以及 <ToggleOff>

static 是 ES6 中 Class 定義靜態方法的關鍵字。靜態方法的好處是在基於類建立新的例項時,新的例項是不會繼承靜態方法的。(簡單來說,不是在原型鏈上定義的,而是直接掛載在這個類的屬性中)

這麼一看 Static 裡面的靜態屬性引入了外部定義的類(如 ToggleOn)

function ToggleOn({on, children}) {
  return on ? children : null
}
function ToggleOff({on, children}) {
  return on ? null : children
}
function ToggleButton({on, toggle, ...props}) {
  return (
    <Switch on={on} onClick={toggle} {...props} />
  )
}
複製程式碼

通過 Static 關聯元件,是有原因的。

使用 React.cloneElement 減少子元件相同props傳遞

<Toggle.On> 等類似的元件中,我們發現 render 中並沒有直接傳入 props ,而 ToggleOn 元件就是接受了props,第一個物件為 on, 第二個則是包裹的 children。

程式碼產生的實際效果應該是這樣的。

render() {
	<Toggle.On on={this.state.on}>The button is on</Toggle.On>
}
複製程式碼

我們再把思路回溯到 Toggle 的 render 這一方法。

render() {
  const children = React.Children.map(
    this.props.children,
    child =>
      React.cloneElement(child, {
        on: this.state.on,
        toggle: this.toggle,
      }),
  )
  return <div>{children}</div>
}
複製程式碼

React.cloneElement

React.cloneElement 接受一個元件,以及可選的props以及children。它的作用就是 clone 一份原先元件並將其替換成新元件。原先元件的 props 會被新傳入的 props 淺合併。而新的children 會替換成原來的 children, 不過元件原先的 key 和 ref 會得到保留。

React.cloneElement(
  element,
  [props],
  [...children]
)
複製程式碼

它的實際作用類似於

<element.type {...element.props} {...props}>{children}</element.type>
複製程式碼

由於 this.props.chidlren 的資料型別是不確定的,可能是個 Object,也可能是 Array.官方文件推薦使用 React.Chidlren 為 children 提供特定的資料操作。

新手很有可能編寫的程式碼結構

如果是我自己寫的話,基本就是以下結構,並且我相信大部分人一上來也是先寫了這段程式碼,後面再開始優化。

class Toggle extends React.Component {
  static defaultProps = {onToggle: () => {}}
  state = {on: false}
  toggle = () =>
    this.setState(
      ({on}) => ({on: !on}),
      () => this.props.onToggle(this.state.on),
    )
  render() {
    const { on } = this.state
    return (<div>
    <ToggleOn on={on}>The button is on</ToggleOn>
    <ToggleOff on={on}>The button is off</ToggleOff>
    <ToggleButton toggle={this.props.onToggle} />
    </div>
    )
  }
}

function App() {
  return (
  <Toggle onToggle={on => console.log(`toggle`, on)} />
  )
}

複製程式碼

我們再對比下就會發現,通過 cloneElemnt 確實可以讓我們的結構更加清晰。 而且,還可以少一個包裹的 div 標籤。雖然在 React16.2中出現了 <><> 的Fragment 表示可以避免此類問題,但是假設如果還有類似的10個元件需要接受相同的 props,我們要寫10遍 on={on},我們應該減少重複勞動。

分析

如果有批量的子元件需要傳遞相同的props,那麼使用 cloneElemnt 再好不過了。但需要注意的是,這也會給子元件傳遞一些不必要的資料。比如在這裡的 demo 中,ToggleOn 和 ToggleOff 只需要接受 on/off 的props 即可,傳入的 toggle 方法是冗餘的。我們假設它又接受了與自身所需資料無關的 其他 props, 而該 props 又頻繁變更,那麼每一次都會得到這些 props 的元件都會 re-render 的可能。

參考資料

  1. https://reactjs.org/docs/react-api.html#cloneelement
  2. https://doc.react-china.org/docs/react-api.html
  3. https://mxstbr.blog/2017/02/react-children-deepdive/