React元件化複用的一些技巧
複用是元件化開發體系的立命之本,可以說元件化的初衷就是為了複用性。但是元件化的複用方式也存在一定的問題,其中拆分粒度就是其中一個繞不開的話題,今天咱們就來講一講 React 當中的一個不太常用的 API: cloneElement
,他如何幫組我們更好得進行元件拆分。
假如我們有一個 Layout
元件,那麼一般來說這個元件主要接收的就是 children
,把它放在主要內容的部分,然後元件本身的節點來控制佈局,那麼這個時候如果我們這個佈局包含兩個部分呢,比如還有一個 header
部分,是跟主要內容有明顯區分的。
比如:

那麼我們這個時候會如何設計這個元件呢?
版本一
function Layout({ header: Header, children }) { return ( <div className='container'> <div className='header'> <Header /> </div> <div classNmae='content'>{children}</div> </div> ) } 複製程式碼
這應該是我們比較常見的方式,我們通過把具體元件作為 Layout
的 props
傳入進來,然後按照元件的寫法把它寫入到元件渲染內容之中。
我們想要使用這個元件,一般會像下面這樣:
function Header() { return <h1>Title Here</h1> } ;<Layout header={Header}> <div>content here</div> </Layout> 複製程式碼
那麼這樣做有什麼問題呢?顯然是有的,最明顯的就是無法在使用 Header
的時候指定 props
如果 Header
有 props
,那麼就我們只能硬編碼在 Layout
裡面,不能在使用 Header
元件的地方進行宣告,所以如果我們想要複用一個 Header
元件,我們可能需要再宣告一個元件,比如我們給 Header
元件一個叫做 message
的 prop
用來指定顯示的文字內容
function Header({ message = 'Title Here' }) { return <h1>{message}</h1> } 複製程式碼
那麼如果我們想要在不同頁面複用這個元件並且顯示不同的標題,我們需要這麼做:
function BigHeader() { return <Header message='The Other Title' /> } 複製程式碼
這麼做顯然在元件較為複雜而且 props
較多的情況下,也可以達到一定的複用效果,但是追求極致的我們肯定不希望僅僅侷限於此。
第二版
那麼有沒有辦法讓我們可以在使用時能指定 props
呢?答案肯定是有的,我們可以將 Layout
的 header
這個 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
再加上key
和ref
形成的字典物件 -
children
,子節點內容,可以是ReactElement
、Array
、string
等內容
最後他返回的是一個叫做 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
元件如果還有另外一個 prop
叫 color
,用來指定文字內容的顯示顏色:
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
,並把後面兩個引數覆蓋原 element
的 props
,以此建立一個新的 ReactElement
。
那麼到此,我們的優化過程也差不多來,當然 demo 顯然是非常簡單的程式碼,現實中的問題往往要複雜很多,比如接收的如果不是一個 ReactElement
而是陣列,字串該如何處理。那麼這些問題在這裡就不再繼續深入來,留給各位小夥伴自己去思考吧,畢竟萬變不離其宗,知道了核心思路之後,其他問題也就可以迎刃而解來。