Render Props
基本用法
有這樣一種組件,它只維護(hù)自身的state(可能也包括修改state值的ui與handler),具體的顯示邏輯由子組件完成。例如:
class Container extends React.Component {
state = { value: 1 }
render() {
return <div>
<button onClick={() => this.setState({ value: this.state.value + 1 })}>
add
</button>
{/* 接收value并展示具體ui的子組件, 例如要渲染ValueUI */}
<ValueUI value={this.state.value} />
</div>
}
}
class ValueUI extends React.Component {
render() {
return <p>{ this.props.value }</p>
}
}
假如我們要渲染的ui組件是可替換的,那么就不能再Container里把它寫死。解決方法就是Render Props寫法:可以向Container傳入一個prop,它是一個輸出組件的function。例如:
class Container extends React.Component {
state = { value: 1 }
render() {
return <div>
<button onClick={() => this.setState({ value: this.state.value + 1 })}>
add
</button>
{ this.props.render(value) }
</div>
}
}
function Main() {
return <Container
render={value => <ValueUI value={value} />}
/>
}
這樣在使用Container時(shí),就可以自由地切換render函數(shù)中要渲染的組件了。
Render Props寫法并不是非要用render作prop名,可以是任意的名稱,還可以利用children屬性,這樣代碼就可以寫成:
class Container extends React.Component {
state = { value: 1 }
render() {
return <div>
<button onClick={() => this.setState({ value: this.state.value + 1 })}>
add
</button>
{ this.props.children(value) }
</div>
}
}
function Main() {
return <Container>{
value => <ValueUI value={value} />
}</Container>
}
重渲染問題
Render Props與PureComponent搭配使用時(shí)有一個問題需要注意。我們把上面的Container組件改為PureComponent,并修改一下Main的代碼:
class Container extends React.PureComponent {...}
class Main extends React.Component {
render() {
return <div>
<button onClick={() => this.setState({})}>click</button>
<Container>{
value => <ValueUI value={value} />
}</Container>
</div>
}
}
點(diǎn)擊Main里的button時(shí),我們預(yù)期的是Container不會重渲染,但是事與愿違。這是因?yàn)镸ain的render函數(shù)每次執(zhí)行時(shí),都會新生成一個value => <ValueUI value={value} />函數(shù),即Container的children屬性,也就是說每次Main重渲染,Container的prop都會改變。解決方法是把這個函數(shù)獨(dú)立出來,比如:
class Main extends React.Component {
renderValueUI(value) {
return <ValueUI value={value} />
}
render() {
return <div>
...
<Container>{ this.renderValueUI }</Container>
</div>
}
}
React 16的新型Context API
Context與Render Props
Render Props寫法也常用于React的新Context API,尤其是嵌套訪問Context時(shí):
const ColorContext = React.createContext('red')
const SizeContext = React.createContext('large')
class Main extends React.Component {
render() {
return <ColorContext.Provider value='red'>
<SizeContext.Provider value='large'>
<Wrapper />
</SizeContext.Provider>
</ColorContext.Provider>
}
}
class Wrapper extends React.Component {
render() {
return <UIComp />
}
}
class UIComp extends React.Component {
render() {
return <ColorContext.Consumet>{
color => (
<SizeContext.Consumer>{
size => <Child color={color} size={size} />
}</SizeContext.Consumer>
)
}</ColorContext.Consumet>
}
}
如果沒有嵌套的話,用contextType的寫法更簡單:
const ColorContext = React.createContext('red')
class Main extends React.Component {
render() {
return <ColorContext.Provider value='red'>
<Wrapper />
</ColorContext.Provider>
}
}
class Wrapper extends React.Component {
render() {
return <UIComp />
}
}
class UIComp extends React.Component {
static contextType = ColorContext
render() {
return <Child color={this.context} />
}
}
Provider的穿透性
Provider本身是一個組件,當(dāng)它所處層級發(fā)生重渲染時(shí),它與普通組件一樣會觸發(fā)自身與children的重渲染;另一方面,只要Provider的value發(fā)生改變(使用Object.is來判斷),它的子樹上的所有Consumer也都會發(fā)生重渲染,不論P(yáng)rovider與Consumer層級之間是否有組件block了更新(shouldComponentUpdate返回false)。這就是新Context API的穿透性,當(dāng)Provider的value改變時(shí),它無視子樹上的shouldComponentUpdate,穿透層級更新Consumer。這個穿透性,老Context是不具備的。
下面是一個驗(yàn)證穿透性與普通組件屬性的例子:
const defaultValue = { count: 1 }
const Context = React.createContext({ ...defaultValue })
class Main extends React.Component {
state = { ...defaultValue }
render() {
return <div>
<div><button onClick={() => {
// # test code #
}}>+</button></div>
<Context.Provider value={this.state.count}>
<div className="app">
<Wrapper count={this.state.count} />
<OtherComp />
</div>
</Context.Provider>
</div>
}
}
class OtherComp extends React.Component {
render() {
console.log('render otherComp')
return <p>OtherComp</p>
}
}
class Wrapper extends React.Component {
shouldComponentUpdate() { return false }
render() {
console.log('render wrapper')
return <div>
<p>wrapper: {this.props.count}</p>
<Child />
</div>
}
}
class Child extends React.Component {
static contextType = Context
render() {
console.log('render child')
return <p>child: {this.context.count}</p>
}
}
# test1: count變化 #
this.setState({ count: this.state.count + 1 })
// click button then logs:
// render child
// render otherComp
// Main重渲染,Provider發(fā)現(xiàn)value變化,無視Wrapper的block,觸發(fā)Consumer(Child)重渲染
# test2: count值不變的情況下重渲染Main #
this.setState(this.state)
// click button then logs:
// render otherComp
// Provider發(fā)現(xiàn)value無變化,不觸發(fā)Consumer重渲染;且Wrapper阻止了重渲染,所以Child沒有重渲染
Context導(dǎo)致的重復(fù)渲染問題:
上面這個例子中,Main組件的state作為Provider的value傳給Consumer,state變更在Main組件的層級發(fā)生,這樣會導(dǎo)致一個問題:每次setState,Main的render函數(shù)執(zhí)行,OtherComp這個不相關(guān)的組件也重渲染了,而我們的本意只是要更新Consumer。換句話說,Main執(zhí)行render -> render中的JSX重新解析(React.CreateElement) -> OtherComp執(zhí)行render。那么有什么方法可以避免呢?
思路很簡單,Main的render執(zhí)行肯定會引發(fā)OtherComp的render執(zhí)行,那么我們可以不讓Main來做管理Context的value這件事:建一個獨(dú)立的組件來管理value和Provider,把children的JSX寫在這個組件之外。
class CountProvider extends React.Component {
state = { ...defaultValue }
render() {
return <div>
<div><button onClick={() => { ... }}>+</button></div>
<Context.Provider value={this.state.count}>
{ this.props.children }
</Context.Provider>
</div>
}
}
class Main extends React.Component {
render() {
return <CountProvider>
<div className="app">
<Wrapper />
<OtherComp />
</div>
</CountProvider>
}
}
這樣以來,CountProvider每次更新state,執(zhí)行render時(shí),收到的children都是Main傳給他的,而Main的render沒有執(zhí)行,因此OtherComp的render也不會執(zhí)行;Context發(fā)現(xiàn)value變化,觸發(fā)子組件樹中的Consumer-Child重渲染。
References
React - Render Props官方文檔
React - Context官方文檔
避免React Context導(dǎo)致的重復(fù)渲染