在典型的React應用中,數據是通過props屬性自上而下(由父及子)進行傳遞的。
但是,在實際開發中,我們發現有某些屬性是許多組件都需要的,那么通過組件樹逐層傳遞props就會特別繁瑣,且不容易維護。
所以總結以下三種不用逐層傳遞props,也可以獲得共享數據的方法:
1、Context
版本: 從React 16.3.0開始,加入了Context API。
Context provides a way to pass data through the component tree without having to pass props down manually at every level. API: 具體可查閱官方文檔https://zh-hans.reactjs.org/docs/context.html#reactcreatecontext React.createContext Context.Provider Context.contextType Context.Consumer Context.displayName
應用場景: 很多不同層級的組件需要訪問同樣一些數據。 例如當前主題、locale等。
應用:Context共享當前主題
ThemeContext.js import { createContext } from "react"; const ThemeContext = createContext(); ThemeContext.displayName = "themeContext"; export default ThemeContext; App.js import React, { Component } from "react"; import ThemeContext from "./ThemeContext"; import ClearButton from "./ClearButton"; class App extends Component { render() { return ( <ThemeContext.Provider value="dark"> <ClearButton /> </ThemeContext.Provider> ); } } ClearButton.js import React, { Component } from 'react'; import ThemeContext from "./ThemeContext"; class ClearButton extends Component{ componentDidMount(){ console.log(this.context) } handleClear=()=>{ } render() { return ( <Tooltip placement="bottom" title="清空畫布"> <span className="handlerbar-icon-box"> <a className={`${this.context} top-icon-button clear-icon`} onClick={this.handleClear} ></a> </span> </Tooltip> ) } ); } } ClearButton.contextType = ThemeContext; export default ClearButton;
掛載在class上的contextType屬性會被重新賦值為一個由React.createContext()創建的Context對象,這可以使我們使用this.context來拿到最近Context上的那個值,並且可以在任何生命周期中訪問到它,包括在render函數中。
如果你正在使用public class fields語法,你可以使用static這個類屬性來初始化你的contextType.如下:
class ClearButton extends Component{ static contextType = ThemeContext; render(){ console.log(this.context) .... } }
如果是函數式組件,你該怎么訂閱context呢?
可以使用Context.Consumer,他需要函數作為子元素, 這個函數接收當前的context值,返回一個React節點
<ThemeContext.Consumer> {value => ( <span>{value}</span> )} </ThemeContext.Consumer>
在React DevTools中可看到如上圖, themeContext就是我們為了好區分其他的Context而起的名字
ThemeContext.displayName = "themeContext"
React DevTools使用該字符串來確定context要顯示的內容。
應用:react-redux中的context
從React 16.3.0開始,Context API包含了2個特殊的組件<Provider>和<Consumer>。如果用過react-redux,會覺得其中的一個很熟悉。那就是<Provider>。因為react-redux也提供了一個叫<Provider>的組件。實際上,react-redux和react提供了兩個大致上一樣的東西。
react-redux是使用了React的Context特性使Redux的Store共享給嵌入的組件。react-redux中,唯一默認的Context對象實例被React.createContext()創建,被叫做ReactReduxContext.
react-redux中Provider.js中部分代碼:
react-redux的<Provider>組件用<ReactReduxContext.Provider>把Redux的store數據放入Context 中,connect用<ReactReduxContext.Consumer>拿到這些值進行一些更新處理。
react-redux中ConnectAdvanced.js中部分代碼:
在使用Context 之前請先考慮下面2點:
Context如上所說主要的應用於很多不同層級的組件需要訪問同樣一些數據。 所以請謹慎使用,因為這會使組件的復用性變差。
如果你只是想避免層層傳遞一些屬性,組件組合(Component composition)有時候是一個比context 更好的解決方案。所以請繼續看第二種方法吧!
2、component composition
應用場景: 子組件與父組件解耦。
組件可以接收任意props,包括基本的數據類型(Number、String、Boolean、Array、Object)、React元素以及函數。
這句話很重要,尤其是props可以是React元素。要先有這個意識才能自己寫出來這樣的組件。
下面這張草圖是商城類的網站,可以先考慮一下要怎樣才能寫出比較好的組件。
這三個List,普通(例如首頁、分類等)的商品列表、訂單中的商品列表、購物車里的商品列表,每一個Item很像卻又有不一樣的地方。用組合組件的思路,我會封裝成3個組件,如下:
一個通用的商品Card組件,但是它的header、leftContent、rightContent、footer是可以作為React元素通過Props傳進來,正如我們在眾多的UI框架中看到的slot.
Stepper組件:用戶選擇、增減商品數量
Button組件:cancel、退貨按鈕等風格統一的按鈕。
Stepper、Button、checkBox作為React元素,通過rightContent、footer、leftContent的props屬性傳給Card組件。自從有了這個思路,我很鄙視我之前的做法,因為不是真正的可復用封裝。瞅一下以前的做法:那就是List里面嵌套Card,Card組件里面又硬編碼嵌套了Stepper組件,比如說現在要根據list組件里面的某個值,判斷Stepper組件的UI狀態,那么你可能就需要List通過props傳給Card,Card又通過props傳給Stepper組件。而通過把Stepper組件作為prop(react元素)動態的傳給Card,那么Stepper組件就會和Card處於同一級別的組件,List=>Card+Stepper這樣就不會出現props的多層傳遞。
3、render props
render prop是指一種在 React 組件之間使用一個值為函數的 prop 共享代碼的簡單技術,更具體地說,render prop 是一個用於告知組件需要渲染什么內容的函數 prop。
重要的是要記住,render prop 是因為模式才被稱為render prop ,你不一定要用名為render的 prop 來使用這種模式。事實上,任何被用於告知組件需要渲染什么內容的函數 prop 在技術上都可以被稱為 “render prop”。
<DataProvider render={data => ( <h1>Hello {data.target}</h1> )}/> <Mouse children={mouse => ( <p>鼠標的位置是 {mouse.x},{mouse.y}</p> )}/>
它和組件組合component composition不同的是:子組件需要在渲染前和父組件進行一些交流,例如子組件要分享父組件中的state。
使用 render prop 的庫有React Router
下面是官網的一個貓追逐鼠標的例子,貓追逐鼠標,自然要監聽鼠標的位置x,y。雖然Cat作為子組件硬編碼到Mouse中,但是它不是一個真正的可復用的封裝。我們可以提供一個帶有prop函數的<Mouse>組件,它能夠動態決定什么需要渲染的,並把state作為參數傳給prop函數。
class Cat extends React.Component { render() { const mouse = this.props.mouse; return ( <img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} /> ); } } class Mouse extends React.Component { constructor(props) { super(props); this.handleMouseMove = this.handleMouseMove.bind(this); this.state = { x: 0, y: 0 }; } handleMouseMove(event) { this.setState({ x: event.clientX, y: event.clientY }); } render() { return ( <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}> {/* Instead of providing a static representation of what <Mouse> renders, use the `render` prop to dynamically determine what to render. */} {this.props.render(this.state)} </div> ); } } class MouseTracker extends React.Component { render() { return ( <div> <h1>移動鼠標!</h1> <Mouse render={mouse => ( <Cat mouse={mouse} /> )}/> </div> ); } }
可以使用帶有 render prop 的常規組件來實現大多數高階組件(HOC)
高階組件是一種基於React的組合特性而形成的設計模式,是一個參數是組件,返回值為新組件的函數。例如react-redux中的connectHOC就是一個高階組件,詳細的可以看react-redux源碼。
function withMouse(Component) { return class extends React.Component { render() { return ( <Mouse render={mouse => ( <Component {...this.props} mouse={mouse} /> )}/> ); } } }
掌握了以上三種方法,會讓你開發React應用如魚得水。