React - 從Render Props說到Context API

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ù)渲染

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • It's a common pattern in React to wrap a component in an ...
    jplyue閱讀 3,411評論 0 2
  • 一、前言 React在很早就支持了context,但是在官方文檔中卻不推薦我們使用它。 The vast majo...
    北溟有漁夫閱讀 2,850評論 11 11
  • 膜結(jié)構(gòu)加 工制作階段的工作內(nèi)容,包括裁剪設(shè)計(jì)的原則、裁剪方法、膜材的連接、裁剪下料圖及加工圖、膜面加工制作的工序與...
    拾日十月閱讀 1,656評論 0 0
  • 天冷起來 溫暖和可愛 走在空氣里 和風(fēng)一起飄逸 你的畫作呈現(xiàn)著真理 每一種物質(zhì)在欣喜 行走到哪里都是美麗 落葉叫人...
    龍青閱讀 350評論 1 8
  • 最近時(shí)常憶起故鄉(xiāng),有綿延不絕的青山,天很高,很藍(lán),云朵千變?nèi)f化,這個季節(jié)雨和太陽總是相伴,正好可以撿蘑菇,燴上自制...
    ItsMay閱讀 270評論 0 0

友情鏈接更多精彩內(nèi)容