React元件化複用的一些技巧

Jokcy發表於2019-02-24

複用是元件化開發體系的立命之本,可以說元件化的初衷就是為了複用性。但是元件化的複用方式也存在一定的問題,其中拆分粒度就是其中一個繞不開的話題,今天我們們就來講一講 React 當中的一個不太常用的 API:cloneElement,他如何幫組我們更好得進行元件拆分。

假如我們有一個Layout元件,那麼一般來說這個元件主要接收的就是children,把它放在主要內容的部分,然後元件本身的節點來控制佈局,那麼這個時候如果我們這個佈局包含兩個部分呢,比如還有一個header部分,是跟主要內容有明顯區分的。

比如:

layout

那麼我們這個時候會如何設計這個元件呢?

版本一

function Layout({ header: Header, children }) {
  return (
    <div className='container'>
      <div className='header'>
        <Header />
      </div>
      <div classNmae='content'>{children}</div>
    </div>
  )
}
複製程式碼

這應該是我們比較常見的方式,我們通過把具體元件作為Layoutprops傳入進來,然後按照元件的寫法把它寫入到元件渲染內容之中。

我們想要使用這個元件,一般會像下面這樣:

function Header() {
  return <h1>Title Here</h1>
}

;<Layout header={Header}>
  <div>content here</div>
</Layout>
複製程式碼

那麼這樣做有什麼問題呢?顯然是有的,最明顯的就是無法在使用Header的時候指定props

如果Headerprops,那麼就我們只能硬編碼在Layout裡面,不能在使用Header元件的地方進行宣告,所以如果我們想要複用一個Header元件,我們可能需要再宣告一個元件,比如我們給Header元件一個叫做messageprop用來指定顯示的文字內容

function Header({ message = 'Title Here' }) {
  return <h1>{message}</h1>
}
複製程式碼

那麼如果我們想要在不同頁面複用這個元件並且顯示不同的標題,我們需要這麼做:

function BigHeader() {
  return <Header message='The Other Title' />
}
複製程式碼

這麼做顯然在元件較為複雜而且props較多的情況下,也可以達到一定的複用效果,但是追求極致的我們肯定不希望僅僅侷限於此。

第二版

那麼有沒有辦法讓我們可以在使用時能指定props呢?答案肯定是有的,我們可以將Layoutheader這個prop接收的不是元件本體,而是具體的ReactElement

function Layout({ header, children }) {
  return (
    <div className='container'>
      <div className='header'>{header}</div>
      <div classNmae='content'>{children}</div>
    </div>
  )
}
複製程式碼

那麼我們在使用的時候就可以非常方便得指定props

<Layout header={<Header message='The Other Title' />}>
  <div>Content Here</div>
</Layout>
複製程式碼

要理解我們可以這麼做,首先我們需要弄清楚什麼是ReactElement。因為我們大部分時候寫React元件的時候用的都是JSX,所以很多同學可能並不知道ReactElement的存在。

其實JSX經過babel翻譯之後得到的是如下程式碼:

// jsx
;<div id='id'>content</div>

// js
React.createElement('div', { id: 'id' }, 'content')
複製程式碼

這個函式接收三個引數

  • component具體渲染的元件,包括原生 dom 節點(string)和自定義元件(object)
  • config,包括所有props再加上keyref形成的字典物件
  • children,子節點內容,可以是ReactElementArraystring等內容

最後他返回的是一個叫做ReactElement型別的物件,他會包含後續 React 渲染過程中需要用到的一個節點包含的所有資訊,我們的props.children其實就是最典型的ReactElement

所以在上訴例子中,我們傳入的header就是一個ReactElement,所以可以直接作為其他節點的children而使用。

同時使用這種方式我們還獲得來一個非常大的優勢,那就是我們甚至可以重新定義一個元件,就可以直接使用Layout

<Layout header={<h1>The Other Title</h1>}>
  <div>Content Here</div>
</Layout>
複製程式碼

這樣同樣也是可以行得通的。

那麼是否到這裡我們就大功告成來呢?NO,NO,NO,我們還是有值得優化的地方。

第三版

試想一下,如果我們的Layout中接收來header是一個節點,但是呢他希望對傳入的元件的一些props有強制的要求呢?比如我們的Header元件如果還有另外一個propcolor,用來指定文字內容的顯示顏色:

function Header({ message = 'Title Here', color = 'red' }) {
  return <h1 style={{ color }}>{message}</h1>
}
複製程式碼

Layout要求所有傳入的Header必須顏色是green,顯示我們也可以在使用Header元件的時候自己指定這個prop,但是如果我們需要強制指定的prop很多,而且使用Layout的地方也很多,那麼明顯我們會寫很多重複程式碼,而且如果後面我們需要修改這個要求的時候也會導致多次修改,甚至有些地方忘了修改而導致 bug。那麼這時候我們該怎麼做呢?

我們可以使用一個 API,這個 API 並不常用,但是在這種場景下,他卻非常有用,這就是React.cloneElement,我們來修改一下Layout

function Layout({ header, children }) {
  return (
    <div className='container'>
      <div className='header'>
        {React.cloneElement(header, { color: 'green' })}
      </div>
      <div classNmae='content'>{children}</div>
    </div>
  )
}
複製程式碼

通過這樣,我們真正渲染出來的Header他的props.color就永遠都是green。那麼這個 API 是啥意思呢?

顧名思義,他是用來克隆一個ReactElement,他接收三個引數,第一個是目標element,第二個是props,第三個是children。可見他跟createElement非常像,唯一的區別是第一個引數從元件變成來節點。

他做的事情其實就是拷貝目標element,並把後面兩個引數覆蓋原elementprops,以此建立一個新的ReactElement


那麼到此,我們的優化過程也差不多來,當然 demo 顯然是非常簡單的程式碼,現實中的問題往往要複雜很多,比如接收的如果不是一個ReactElement而是陣列,字串該如何處理。那麼這些問題在這裡就不再繼續深入來,留給各位小夥伴自己去思考吧,畢竟萬變不離其宗,知道了核心思路之後,其他問題也就可以迎刃而解來。

相關文章