組件化開發的時候,參數傳遞是非常關鍵的環節
哪些參數放在組件內部管理,哪些參數由父組件傳入,哪些狀態需要反饋給父組件,都需要在設計組件的時候想清楚
但實現這些交互的基礎,是明白組件之間參數傳遞的方式,和各自的優缺點
一、父組件傳參到子組件
和 Vue 一樣,React 中從父組件到子組件的傳參也是通過 props
不過在 Vue 項目中,需要在先組件里定義將要接收的 props,而 React 可以直接獲取
而且 props 不光可以接收 Number、String 等基本類型,還可以接收 Function,這點在《從 Vue 的視角學 React(三)—— 事件處理》中也有提到
另外 props 還可以直接接收組件,類似於 Vue 中的 slot,后面會詳細介紹
二、子組件向父組件傳參
在項目中,也會有需要更新 props 的時候
但和 Vue 一樣,React 中的 props 也是單向的,一定不能在組件內部直接修改 props 的值
而應該采用事件上報的方式,讓父組件修改相應的狀態
整個過程就像是 Vue 中的 $emit,只是將 $emit 中聲明的自定義事件放到了 props 中
上面的 updateText 就是一個自定義事件,父組件為這個自定義事件添加了處理函數 handleUpdateText
如果子組件需要向父組件傳遞參數,可以在觸發自定義事件 updateText 的時候,向事件處理函數傳參
然后父組件在對應的事件處理函數 handleUpdateText 中獲取到子組件傳過來的參數
三、狀態提升
除了父子之間的傳參,還會有兩個平級組件之間的參數傳遞
React 的推薦方案是,將兩個子組件的參數都放到父組件中處理,這就是狀態提升
假設有 boy 和 girl 兩個組件,它們都是組件 father 的子組件
組件 girl 中有一個 food 狀態,組件 boy 中有一個 age 狀態,food 會隨着 age 的變化而變化
如果 age 和 food 都是對應組件的 state,維護起來就比較麻煩
而如果將 age 和 food 都作為 props 傳入,就可以在父組件 father 中維護這兩個狀態
當需要更新 age 的時候,從 boy 組件上報事件,然后在父組件中同時更新 food 和 age
React 是單向數據流,在設計組件的時候,應當始終保持自上而下的數據傳遞,而不是嘗試在不同組件間同步 state
如果某些數據可以由 props 或 state 推導得出,那么它就不應該存在於 state 中
雖然這種方式會比雙向綁定需要編寫更多的代碼,但更利於維護
四、限制 props 類型
良好的組件不可避免的會用到很多 props,為這些 props 添加類型校驗就尤為重要
Vue 本身就有一套很完善的 props 類型校驗配置,React 之前也有 React.PropTypes
但支持 TypeScript 之后,就將這部分功能拆成了獨立的第三方庫 prop-types
npm install prop-types -S
引入 PropTypes 之后,在組件內定義一個靜態屬性 propTypes(注意大小寫),然后定義具體的規則
參考上圖的示例,校驗規則由 . 語法連接,如果有多個類型,就使用 oneOfType
更多關於 PropTypes 的使用可以查看官方文檔
除了類型校驗,有時候還需要給 props 添加默認值,這時候可以用 defaultProps
五、組件嵌套
雖然開發組件的時候,我們總是希望能盡量滿足需求,以減少后期的工作量
但一萬個讀者就有一萬個哈姆雷特,一個組件不可能滿足所有用戶的需求,所以有時候就需要為組件提供一些擴展性
另外,還有一些組件本身就是作為容器開發的,這些場景都需要將組件作為 props 傳給子組件
const Child = props => <div>{props.content}</div>
const Tag = props => <div>This is tag</div>
const Parents = props => <Child content={<Tag />}/>
上面的代碼中,子組件 Child 會接收一個 props 屬性 content
然后在父組件 Parents 中,將 Tag 組件作為 content 的值傳給了子組件 Child
不過這樣的寫法並不優雅,如果能像 Vue 的 slot 那樣,直接將子組件嵌套在父組件的標簽內,就更符合 HTML 標簽的結構
這時候就可以用到 props.children
const Child = props => <div>{props.children}</div>
const Tag = props => <div>This is tag</div>
const Parents = props => <Child><Tag /></Child>
從上面的代碼可以看出,組件標簽之間嵌套的內容,可以在組件內通過 props.children 接收到
事實上,props.children 是一個數組,如果不加具體的元素下標,就會將所有的元素渲染出來
如果標簽內有多個節點,porps.children 就會將自身組件作為根節點,以數組的形式將組件內的 DOM 結構虛擬出來
而且 props.children 不單可以接收組件,還可以接收字符串
六、Context
在維護大型項目的時候,僅靠 props 和事件來傳參是不夠的,所以 Vue 提供了 Vuex 來維護狀態
React 也有狀態管理工具,常用的有 Redux、Mobx,如何使用這些工具我會在以后的文章中介紹
但在使用這些工具之前,需要了解 context,因為 Redux 和 Mobx 都是基於 context 實現的
如果組件的層級很深,僅使用 props 層層嵌套傳參的話就非常的冗余
這時如果有一個全局變量,在頂層組件定義之后,直接在底層組件中獲取,就會非常簡潔,context 就是這樣的全局變量
// Context 可以讓我們無須明確地傳遍每一個組件,就能將值深入傳遞進組件樹。 // 為當前的 theme 創建一個 context(“light”為默認值)。
const ThemeContext = React.createContext('light'); class App extends React.Component { render() { // 使用一個 Provider 來將當前的 theme 傳遞給以下的組件樹。
// 無論多深,任何組件都能讀取這個值。
// 在這個例子中,我們將 “dark” 作為當前的值傳遞下去。
return ( <ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
); } } // 中間的組件再也不必指明往下傳遞 theme 了。
function Toolbar(props) { return ( <div>
<ThemedButton />
</div>
); } class ThemedButton extends React.Component { // 指定 contextType 讀取當前的 theme context。
// React 會往上找到最近的 theme Provider,然后使用它的值。
// 在這個例子中,當前的 theme 值為 “dark”。
static contextType = ThemeContext; render() { return <Button theme={this.context} />;
} }
但 context 的使用會極大的增強組件之間的耦合性,項目中並不建議直接使用
所以我直接復制粘貼了官方文檔的代碼,僅為了解 context 這個概念
小型項目中,如果有深層次的傳參,應當從組件設計上解決問題,比如直接將組件傳下去
而大型項目中,如果需要用到 context,更推薦使用 redux 和 mobx 這些成熟的狀態管理工具
參考資料: