react中@withrouter_为什么 withRouter 高阶组件应该 处于最外层?
之前在 CR 中看到立理大佬的評(píng)論,說 withRouter 高階組件應(yīng)該放在最外面,不然可能會(huì)造成 url 變化了但是組件沒有渲染的情況,當(dāng)時(shí)并不理解,然后照著 ReactRouter 的文檔仔細(xì)研究了一下原因。
復(fù)現(xiàn)不能正常渲染的情況
React 中有兩種常見提升渲染性能的方式:
下面是一個(gè)簡單的例子:
class UpdateBlocker extends PureComponent {render() {return this.props.children;} }const App = () => (<Router><UpdateBlocker><NavLink to='/about'>About</NavLink><NavLink to='/faq'>F.A.Q.</NavLink></UpdateBlocker></Router> );這個(gè) NavLink 也是 react-router-dom 里面的一個(gè)組件,它也是渲染了一個(gè) a 標(biāo)簽,比較特殊的是,當(dāng)他能匹配當(dāng)前 url 的時(shí)候,會(huì)默認(rèn)給 a 標(biāo)簽再添加一個(gè) active 類。
所以,我們?cè)偬砑舆@樣一段 CSS,然后看效果:
a {display: block; }a.active {color: red; }按照設(shè)想中的效果,應(yīng)該是點(diǎn)擊 About 之后,About 變紅,點(diǎn)擊 F.A.Q. 后,F.A.Q. 變紅。但是現(xiàn)實(shí)卻是點(diǎn)擊之后并沒有什么效果。
原因就是 UpdateBlocker 是一個(gè) PureComponent,想要它重新渲染,除非它的 state 或者 props 有什么大的變動(dòng)才行(淺比較結(jié)果為 false),然而在上面的例子中,UpdateBlocker 并沒有發(fā)生這種變化,所以就理所當(dāng)然的不會(huì)變化了。
shouldComponentUpdate 原理類似。所以在實(shí)現(xiàn)這個(gè)生命周期的時(shí)候,也要考慮 url 變動(dòng)的情況。官方文檔中說可以通過 context.router 來確定 url 是否變動(dòng),但由于用戶不該直接使用 context 所以不建議這么做,而是推薦通過使用傳入 location 屬性的形式。
解決方案
所以,當(dāng)你的 Component 是 Pure 的,應(yīng)該如何處理這種情況呢?
我簡單看了一下 react-router 的實(shí)現(xiàn),當(dāng) <Link> 點(diǎn)擊之后,會(huì)通過 context 觸發(fā) <Router>或者 <Route> 里面實(shí)現(xiàn)的相應(yīng)的函數(shù),然后在 <Router> 或 <Route> 中 setState 觸發(fā)渲染。所以不管是 <Link> 還是 withRouter 這些東西,一定要是 <Router> 或者 <Route>的后代才行。(沒理解錯(cuò)吧)
所以,如果希望 UpdateBlocker 也能正常渲染的話,只要給它傳入一個(gè)能夠觸發(fā)渲染的屬性就好了,比如 location 對(duì)象。只要想辦法在父組件拿到 location 對(duì)象,然后通過屬性給那個(gè) Pure 的組件傳過去。當(dāng) URL 變化時(shí),location 也會(huì)相應(yīng)改變,所以也就不怕 Pure 的組件不渲染了:
<UpdateBlocker location={location}><NavLink to='/about'>About</NavLink><NavLink to='/faq'>F.A.Q.</NavLink> </UpdateBlocker>那么如何讓父組件拿到 location 對(duì)象呢?
直接通過 <Route> 渲染的組件
如果你的組件是直接通過 <Route> 渲染的話:
1. 一個(gè)直接通過 <Route> 渲染的組件,不需要擔(dān)心上面的問題,因?yàn)?<Route> 會(huì)自動(dòng)為其包裹的組件插入 location 屬性。
// 當(dāng) url 變化時(shí),<Blocker> 的 location 屬性一定會(huì)變化 <Route path='/:place' component={Blocker}/>2. 一個(gè)直接通過 <Route> 渲染的組件,既然可以拿到 location 屬性,所以自然也可以把 location 傳給由它創(chuàng)建的子組件。
<Route path='/parent' component={Parent} />const Parent = (props) => {// 既然 <Parent> 能拿到 location 屬性// 自然也可以把 location 傳給由它創(chuàng)建的子組件return (<SomeComponent><Blocker location={props.location} /></SomeComponent>); }不是直接通過 <Route> 渲染的組件
如果一個(gè)組件不是由 <Route> 直接渲染的怎么辦呢?也有兩種辦法:
1. 可以使用不傳 path 屬性的 <Route> 組件。<Route> 組件中的 path 屬性也不是必須的,當(dāng)不傳入 path 屬性時(shí),表示它包裹的組件總會(huì)渲染:
// <Blocker> 組件總會(huì)渲染 const MyComponent= () => (<SomeComponent><Route component={Blocker} /></SomeComponent> );2. 使用 withRouter 高階組件。這個(gè)高階組件就會(huì)給它包裹的組件傳三個(gè)屬性,分別是 location、match 和 history。
const BlockAvoider = withRouter(Blocker)const MyComponent = () => (<SomeComponent><BlockAvoider /></SomeComponent> );其他情況
有時(shí)候即便你沒有使用 PureComponent 也有可能出現(xiàn)上面的問題,因?yàn)槟阌锌赡苁褂昧艘恍?shí)現(xiàn)了 shouldComponentUpdate 的高階組件,比如:
// react-redux const MyConnectedComponent = connect(mapStateToProps)(MyComponent)// mobx-react(這個(gè)我沒用過) const MyObservedComponent = observer(MyComponent)這個(gè) connect 和 observer 都實(shí)現(xiàn)了自己的 shouldComponentUpdate,它們也是對(duì)當(dāng)前的 props 和 nextProps 淺比較,所以也會(huì)導(dǎo)致 即使 url 變化,也無法重新渲染 的情況。
通過上面的分析我們也很容易找到相應(yīng)的解決方案,比如:
const MyConnectedComponent = withRouter(connect(mapStateToProps)(MyComponent))const MyConnectedComponent = withRouter(observer(MyComponent))其實(shí)當(dāng)我看到這里的時(shí)候就已經(jīng)理解為什么 withRouter 要放在最外層了。很好理解,因?yàn)槿绻?withRouter 在 connect 里面,即便能夠給 MyComponent 傳入 location 對(duì)象,可是渲染早在 connect 那一層就被攔截住了...
withRouter 這個(gè)高階組件很好用,但是它并不是所有情景的最佳解決方案,還是應(yīng)該視情況而定。
因?yàn)?withRouter 本身的作用是為了給那些需要操作 route 的組件提供 location 等屬性。如果一個(gè)組件本身就已經(jīng)能拿到這些屬性了,那再使用 withRouter 就是一種浪費(fèi)了。
原文中還舉了一個(gè)常見的錯(cuò)誤操作,即通過 <Route> 包裹的組件,就實(shí)在沒必要包裹一層 withRouter 了:
// 這里的 withRouter 是完全沒必要的 const MyComponent = withRouter(connect(...)(AComponent));<Route path='/somewhere' component={MyComponent} /> /** <Route path='/somewhere>* <withRouter()>* <Route>* <connect()>* <AComponent>*/context 與 shouldComponentUpdate 一起使用
通過上面對(duì)于 react-router 的討論,也可以推廣至其他使用 context 的場景。
在 React 16.3 以前,context 是一個(gè)實(shí)驗(yàn)性的 API,應(yīng)該是盡量避免使用的,起碼要盡量避免直接使用,雖然使用 context 實(shí)現(xiàn)跨級(jí)組件通信很方便。
如果使用 context 實(shí)現(xiàn)了跨級(jí)組件通信,就會(huì)面臨這樣的問題:shouldComponentUpdate 或者 PureComponent 阻止了 context 的 “捕獲”。
import React, {PureComponent, Component} from 'react'; import ReactDOM from 'react-dom'; import {bind} from 'lodash-decorators'; import PropTypes from 'prop-types';class ColorProvider extends Component {static childContextTypes = {color: PropTypes.string};getChildContext() {return {color: this.props.color};}render() {return this.props.children;} }class ColorText extends Component {static contextTypes = {color: PropTypes.string};render() {const {color} = this.context;const {children} = this.props;return (<div style={{color}}>{children}</div>);} }class TextBox extends PureComponent {render() {return <ColorText>TextBox</ColorText>} }class App extends Component {state = {color: 'red'};@bind()handleClick() {this.setState({color: 'blue'});}render() {const {color} = this.state;return (<ColorProvider color={color}><button onClick={this.handleClick}><ColorText>Click Me</ColorText></button><TextBox /></ColorProvider>);} }ReactDOM.render(<App />, document.getElementById("app"));上面這份代碼的效果就是顯示一個(gè)按鈕和一行文字,點(diǎn)擊按鈕后,按鈕顏色變藍(lán),但是下面那行文字卻沒動(dòng)靜。因?yàn)?TextBox 組件是 Pure 的,點(diǎn)擊按鈕后,它的 state 和 props 都沒有變化,所以自然沒有重新渲染。也就是說,這個(gè) Pure 的組件把 context 的傳遞給攔截住了。
如何讓 conext 和 shouldComponent 一起工作呢?我查了相關(guān)資料之后,得到了以下兩種辦法:
1. shouldComponentUpdate 是支持第三個(gè)參數(shù)的...如果 contextTypes 在組件中定義,下列的生命周期方法將接受一個(gè)額外的參數(shù), 就是 context 對(duì)象:
constructor(props, context) componentWillReceiveProps(nextProps, nextContext) shouldComponentUpdate(nextProps, nextState, nextContext) componentWillUpdate(nextProps, nextState, nextContext) componentDidUpdate(prevProps, prevState, prevContext)2. 使用一種類似 EventEmitter 這樣的東西實(shí)現(xiàn):
import React, {PureComponent, Component} from 'react'; import ReactDOM from 'react-dom'; import {bind} from 'lodash-decorators'; import PropTypes from 'prop-types';class Color {constructor(value) {this.value = value;this.depends = [];}depend(f) {this.depends.push(f);}setValue(value) {this.value = value;this.depends.forEach(f => f());} }class ColorProvider extends Component {static childContextTypes = {color: PropTypes.object};getChildContext() {return {color: this.props.color};}render() {return this.props.children;} }class ColorText extends Component {static contextTypes = {color: PropTypes.object};componentDidMount() {this.context.color.depend(() => this.forceUpdate());}render() {const {color} = this.context;const {children} = this.props;return (<div style={{color: color.value}}>{children}</div>);} }class TextBox extends PureComponent {render() {return <ColorText>TextBox</ColorText>} }class App extends Component {color = new Color('red');@bind()handleClick() {this.color.setValue('blue');}render() {return (<ColorProvider color={this.color}><button onClick={this.handleClick}><ColorText>Click Me</ColorText></button><TextBox /></ColorProvider>);} }ReactDOM.render(<App />, document.getElementById("app"));更好的做法是,不要把 context 當(dāng)做 state 用,而是把它作為一個(gè) dependency:
Context should be used as if it is received only once by each component.
在 React 16.3 之后,context 有了正經(jīng)的新 API,在新的 context API 中,使用 React.createContext(defaultValue) 這個(gè) API 來創(chuàng)建 context 對(duì)象,使用 context 對(duì)象中的 <Provider /> 和 <Consumer /> 來操作 context。并且當(dāng) Provider 的值改變時(shí),所有的 Consumer 也都會(huì)重新渲染,所以說,在新的 API 中,已經(jīng)輕松避免了上述問題...
總結(jié)
以上是生活随笔為你收集整理的react中@withrouter_为什么 withRouter 高阶组件应该 处于最外层?的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 同一个页面提交多个form表单方法(详细
- 下一篇: RTX5 | 事件标志组02 - 置位事