这篇文章的主要内容是说 React 16.3 以前的 getChildContext 这个老 Context API 存在会被 PureComponent 截断的问题,React-Router 4.3.1 及之前版本是用这种 API 实现的,所以存在这个问题,在新版本的 React-Router 中已经没有这个问题了。

之前在 CR 中看到立理大佬的评论,说 withRouter 高阶组件应该放在最外面,不然可能会造成 url 变化了但是组件没有渲染的情况,当时并不理解,然后照着 ReactRouter 的文档仔细研究了一下原因。

复现不能正常渲染的情况

React 中有两种常见提升渲染性能的方式:

  1. 通过 shouldComponentUpdate 生命周期控制组件是否重新渲染
  2. 使用 PureComponent

下面是一个简单的例子:

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>
);

这个 NavLink 也是 react-router-dom 里面的一个组件,它也是渲染了一个 a 标签,比较特殊的是,当他能匹配当前 url 的时候,会默认给 a 标签再添加一个 active 类。

所以,我们再添加这样一段 CSS,然后看效果:

a {display: block;
}a.active {color: red;
}

按照设想中的效果,应该是点击 About 之后,About 变红,点击 F.A.Q. 后,F.A.Q. 变红。但是现实却是点击之后并没有什么效果。

原因就是 UpdateBlocker 是一个 PureComponent,想要它重新渲染,除非它的 state 或者 props 有什么大的变动才行(浅比较结果为 false),然而在上面的例子中,UpdateBlocker 并没有发生这种变化,所以就理所当然的不会变化了。

shouldComponentUpdate 原理类似。所以在实现这个生命周期的时候,也要考虑 url 变动的情况。官方文档中说可以通过 context.router 来确定 url 是否变动,但由于用户不该直接使用 context 所以不建议这么做,而是推荐通过使用传入 location 属性的形式。

解决方案

所以,当你的 Component 是 Pure 的,应该如何处理这种情况呢?

我简单看了一下 react-router 的实现,当 <Link> 点击之后,会通过 context 触发 <Router>或者 <Route> 里面实现的相应的函数,然后在 <Router><Route> 中 setState 触发渲染。所以不管是 <Link> 还是 withRouter 这些东西,一定要是 <Router> 或者 <Route>的后代才行。(没理解错吧)

所以,如果希望 UpdateBlocker 也能正常渲染的话,只要给它传入一个能够触发渲染的属性就好了,比如 location 对象。只要想办法在父组件拿到 location 对象,然后通过属性给那个 Pure 的组件传过去。当 URL 变化时,location 也会相应改变,所以也就不怕 Pure 的组件不渲染了:

<UpdateBlocker location={location}><NavLink to='/about'>About</NavLink><NavLink to='/faq'>F.A.Q.</NavLink>
</UpdateBlocker>

那么如何让父组件拿到 location 对象呢?

直接通过 <Route> 渲染的组件

如果你的组件是直接通过 <Route> 渲染的话:

1. 一个直接通过 <Route> 渲染的组件,不需要担心上面的问题,因为 <Route> 会自动为其包裹的组件插入 location 属性。

// 当 url 变化时,<Blocker> 的 location 属性一定会变化
<Route path='/:place' component={Blocker}/>

2. 一个直接通过 <Route> 渲染的组件,既然可以拿到 location 属性,所以自然也可以把 location 传给由它创建的子组件。

<Route path='/parent' component={Parent} />const Parent = (props) => {// 既然 <Parent> 能拿到 location 属性// 自然也可以把 location 传给由它创建的子组件return (<SomeComponent><Blocker location={props.location} /></SomeComponent>);
}

不是直接通过 <Route> 渲染的组件

如果一个组件不是由 <Route> 直接渲染的怎么办呢?也有两种办法:

1. 可以使用不传 path 属性的 <Route> 组件。<Route> 组件中的 path 属性也不是必须的,当不传入 path 属性时,表示它包裹的组件总会渲染:

// <Blocker> 组件总会渲染
const MyComponent= () => (<SomeComponent><Route component={Blocker} /></SomeComponent>
);

2. 使用 withRouter 高阶组件。这个高阶组件就会给它包裹的组件传三个属性,分别是 locationmatchhistory

const BlockAvoider = withRouter(Blocker)const MyComponent = () => (<SomeComponent><BlockAvoider /></SomeComponent>
);

其他情况

有时候即便你没有使用 PureComponent 也有可能出现上面的问题,因为你有可能使用了一些实现了 shouldComponentUpdate 的高阶组件,比如:

// react-redux
const MyConnectedComponent = connect(mapStateToProps)(MyComponent)// mobx-react(这个我没用过)
const MyObservedComponent = observer(MyComponent)

这个 connectobserver 都实现了自己的 shouldComponentUpdate,它们也是对当前的 props 和 nextProps 浅比较,所以也会导致 即使 url 变化,也无法重新渲染 的情况。

通过上面的分析我们也很容易找到相应的解决方案,比如:

const MyConnectedComponent = withRouter(connect(mapStateToProps)(MyComponent))const MyConnectedComponent = withRouter(observer(MyComponent))

其实当我看到这里的时候就已经理解为什么 withRouter 要放在最外层了。很好理解,因为如果 withRouterconnect 里面,即便能够给 MyComponent 传入 location 对象,可是渲染早在 connect 那一层就被拦截住了...

withRouter 这个高阶组件很好用,但是它并不是所有情景的最佳解决方案,还是应该视情况而定。

因为 withRouter 本身的作用是为了给那些需要操作 route 的组件提供 location 等属性。如果一个组件本身就已经能拿到这些属性了,那再使用 withRouter 就是一种浪费了。

原文中还举了一个常见的错误操作,即通过 <Route> 包裹的组件,就实在没必要包裹一层 withRouter 了:

// 这里的 withRouter 是完全没必要的
const MyComponent = withRouter(connect(...)(AComponent));<Route path='/somewhere' component={MyComponent} />
/** <Route path='/somewhere>*   <withRouter()>*     <Route>*       <connect()>*         <AComponent>*/

context 与 shouldComponentUpdate 一起使用

通过上面对于 react-router 的讨论,也可以推广至其他使用 context 的场景。

在 React 16.3 以前,context 是一个实验性的 API,应该是尽量避免使用的,起码要尽量避免直接使用,虽然使用 context 实现跨级组件通信很方便。

如果使用 context 实现了跨级组件通信,就会面临这样的问题: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"));

上面这份代码的效果就是显示一个按钮和一行文字,点击按钮后,按钮颜色变蓝,但是下面那行文字却没动静。因为 TextBox 组件是 Pure 的,点击按钮后,它的 state 和 props 都没有变化,所以自然没有重新渲染。也就是说,这个 Pure 的组件把 context 的传递给拦截住了。

如何让 conext 和 shouldComponent 一起工作呢?我查了相关资料之后,得到了以下两种办法:

1. shouldComponentUpdate 是支持第三个参数的...如果 contextTypes 在组件中定义,下列的生命周期方法将接受一个额外的参数, 就是 context 对象:

constructor(props, context)
componentWillReceiveProps(nextProps, nextContext)
shouldComponentUpdate(nextProps, nextState, nextContext)
componentWillUpdate(nextProps, nextState, nextContext)
componentDidUpdate(prevProps, prevState, prevContext)

2. 使用一种类似 EventEmitter 这样的东西实现:

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 当做 state 用,而是把它作为一个 dependency:

Context should be used as if it is received only once by each component.

在 React 16.3 之后,context 有了正经的新 API,在新的 context API 中,使用 React.createContext(defaultValue) 这个 API 来创建 context 对象,使用 context 对象中的 <Provider /><Consumer /> 来操作 context。并且当 Provider 的值改变时,所有的 Consumer 也都会重新渲染,所以说,在新的 API 中,已经轻松避免了上述问题...

react中@withrouter_为什么 withRouter 高阶组件应该 处于最外层?相关推荐

  1. [react] 举例说明什么是高阶组件(HOC)的反向继承

    [react] 举例说明什么是高阶组件(HOC)的反向继承 import React from 'react';const hoc = (WrappedComponent) => {// 集成需 ...

  2. [react] 举例说明什么是高阶组件(HOC)的属性代理

    [react] 举例说明什么是高阶组件(HOC)的属性代理 function HOC(WrappedComponent) {return class HOC extends Component {re ...

  3. React总结篇之六_React高阶组件

    高阶组件的概念及应用 以函数为子组件的模式 这两种方式的最终目的都是为了重用代码,只是策略不同,各有优劣,开发者可以在实际工作中决定采用哪种方式. 一.高阶组件 1. 高阶组件(Higher Orde ...

  4. react ref无法获取被高阶组件包装的原始组件问题

    问题描述: react无法通过ref获取被高阶组件包装的原始组件 通过ref调用被dva connect包裹的组件报错 我们在平时使用ref获取一个组件的引用后,就可以直接通过ref调用组件自身的函数 ...

  5. React中的高阶组件

    React中的高阶组件 高阶组件HOC即Higher Order Component是React中用于复用组件逻辑的一种高级技巧,HOC自身不是React API的一部分,它是一种基于React的组合 ...

  6. create React-app脚手架中封装withRouter.js高阶组件

    由于 useNavigate.useLocation.useSearchParams等方法,只能写在函数内,也就是说我们可以在React Hooks 函数组件内可以直接 import 导入,创建实例化 ...

  7. React 中的高阶组件及其应用场景

    本文目录 什么是高阶组件 React 中的高阶组件 属性代理(Props Proxy) 反向继承(Inheritance Inversion) 高阶组件存在的问题 高阶组件的约定 高阶组件的应用场景 ...

  8. react实现汉堡_利用 React 高阶组件实现一个面包屑导航

    什么是 React 高阶组件 React 高阶组件就是以高阶函数的方式包裹需要修饰的 React 组件,并返回处理完成后的 React 组件.React 高阶组件在 React 生态中使用的非常频繁, ...

  9. web前端高级React - React从入门到进阶之高阶组件

    第二部分:React进阶 系列文章目录 第一章:React从入门到进阶之初识React 第一章:React从入门到进阶之JSX简介 第三章:React从入门到进阶之元素渲染 第四章:React从入门到 ...

最新文章

  1. NETMerger - DotNet 组件(dll或exe)合并
  2. java中上传附件怎么该名称_将附件保存到Outlook中的文件夹并重命名
  3. ArcGIS AO中控制图层中要素可见状态的总结
  4. 前端之 XMLHttpRequest
  5. 用户、组织结构、功能菜单、权限分配设计
  6. C# interface 理解 数据库统一接口
  7. altium09怎么查元器件_长文图解:单张表数据量太大问题怎么解决?请记住这六个字...
  8. JS继承之寄生类继承
  9. 开源跨平台计算机视觉库OpenCV 4.0正式发布
  10. 2022年起重机械指挥判断题及答案
  11. Trace-导出已有的服务器端跟踪
  12. 和Keyle一起学ShaderForge - Overview
  13. 《汉字简体、繁体相互转换》 查看源代码
  14. Java进阶之反射机制
  15. Ubiquitous Religions
  16. 使用TestCafe进行UI测试
  17. 广搜4 ——Cheese
  18. ERP中英文缩写汇总
  19. 算法与数据结构入门一篇就搞定
  20. switch细节讨论

热门文章

  1. 聊聊ExecutorService的监控
  2. 将Tomcat添加进服务启动
  3. SSH框架中怎么使用Hibernate查询一个对象
  4. PHP 基本语法,变量
  5. jQuery模拟原生态App上拉刷新下拉加载
  6. HTML DOM教程 19-HTML DOM Button 对象
  7. Frontpage网页制作软件,你们还记得吗?
  8. 《飞鸽传书2007绿色版下载》总结报告
  9. ATL offsetofclass 的工作原理
  10. 自从用python写了个自动弹幕脚本后,各大主播来找我,净赚十万!赶紧收藏!!