本文梳理了容器与展示组件、高阶组件、render props这三类React组件设计模式
一、容器(Container)与展示(Presentational)组件
概念介绍
示例
我们来看一个简单的例子。构造一个组件,该组件的作用是获取文本并将其展示出来。
export default class GetText extends React.Component { state = { text: null, } componentDidMount() { fetch('https://api.com/', { headers: { Accept: 'application/json' } }).then(response => { return response.json() }).then(json => { this.setState({ text: json.joke }) }) } render() { return (<div> <div>外部获取的数据:{this.state.text}</div> <div>UI代码</div></div> ) }}
看到上面 GetText 这个组件,当有和外部数据源进行沟通的逻辑。那么我们就可以把这个组件拆成两部分。
一部分专门负责和外部通信(容器组件),一部分负责UI逻辑(展示组件)。我们来将上面那个例子拆分看看。
容器组件:
export default class GetTextContainer extends React.Component { state = { text: null, } componentDidMount() { fetch('https://api.com/', { headers: { Accept: 'application/json' } }).then(response => { return response.json() }).then(json => { this.setState({ text: json.joke }) }) } render() { return (<div><GetTextPresentational text={this.state.text}/></div> ) }}
展示组件:
export default class GetTextPresentational extends React.Component { render() { return (<div> <div>外部获取的数据:{this.props.text}</div> <div>UI代码</div></div> ) }}
模式所解决的问题
二、高阶组件
概念介绍
示例
前面我们已经说过了,高阶组件其实是利用一个函数,接受 React 组件作为参数,然后返回新的组件。
我们这边新建一个 judgeWoman 函数,接受具体的展示组件,然后判断是否是女性,
const judgeWoman = (Component) => { const NewComponent = (props) => {// 判断是否是女性用户 let isWoman = Math.random() > 0.5 ? true : false if (isWoman) { const allProps = { add: '高阶组件增加的属性', ...props } return <Component {...allProps} />; } else { return <span>女士专用,男士无权浏览</span>; } } return NewComponent;};
再将 List 和 ShoppingCart 两个组件作为参数传入这个函数。至此,我们就得到了两个加强过的组件 WithList 和 WithShoppingCart.判断是否是女性的这段逻辑得到了复用。
const List = (props) => { return (<div> <div>女士列表页</div> <div>{props.add}</div></div> )}const WithList = judgeWoman(List)const ShoppingCart = (props) => { return (<div> <div>女士购物页</div> <div>{props.add}</div></div> )}const WithShoppingCart = judgeWoman(ShoppingCart)
const judgeWoman = (Woman,Man) => { const NewComponent = (props) => {// 判断是否是女性用户 let isWoman = Math.random() > 0.5 ? true : false if (isWoman) { const allProps = { add: '高阶组件增加的属性', ...props } return <Woman {...allProps} />; } else { return <Man/> } } return NewComponent;};
更为强大的是,由于函数返回的也是组件,那么高阶组件是可以嵌套进行使用的!比如我们先判断性别,再判断年龄。
const withComponet =judgeAge(judgeWoman(ShoppingCart))
具体代码可见 src/pattern2(http://t.cn/AiYbYy5g)
模式所解决的问题
同样的逻辑我们总不能重复写多次。高阶组件起到了抽离共通逻辑的作用。同时高阶组件的嵌套使用使得代码复用更加灵活了。
react-redux 就使用了该模式,看到下面的代码,是不是很熟悉?connect(mapStateToProps, mapDispatchToProps)生成了高阶组件函数,该函数接受 TodoList 作为参数。最后返回了 VisibleTodoList 这个高阶组件。
import { connect } from 'react-redux'const VisibleTodoList = connect( mapStateToProps, mapDispatchToProps)(TodoList)
使用注意事项
高阶组件虽好,我们使用起来却要注意如下点。
1、包装显示名称以便轻松调试
使用高阶组件后 debug 会比较麻烦。当 React 渲染出错的时候,靠组件的 displayName 静态属性来判断出错的组件类。HOC 创建的容器组件会与任何其他组件一样,会显示在 React Developer Tools 中。为了方便调试,我们需要选择一个显示名称,以表明它是 HOC 的产物。
最常见的方式是用 HOC 包住被包装组件的显示名称。比如高阶组件名为withSubscription,并且被包装组件的显示名称为 CommentList,显示名称应该为WithSubscription(CommentList):
function withSubscription(WrappedComponent) { class WithSubscription extends React.Component {/* ... */} WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`; return WithSubscription;}function getDisplayName(WrappedComponent) { return WrappedComponent.displayName || WrappedComponent.name || 'Component';}
2、不要在 render 方法中使用 HOC
React 的 diff 算法(称为协调)使用组件标识来确定它是应该更新现有子树还是将其丢弃并挂载新子树。 如果从 render 返回的组件与前一个渲染中的组件相同(===),则 React 通过将子树与新子树进行区分来递归更新子树。 如果它们不相等,则完全卸载前一个子树。
通常,你不需要考虑这点。但对 HOC 来说这一点很重要,因为这代表着你不应在组件的 render 方法中对一个组件应用 HOC:
render() {// 每次调用 render 函数都会创建一个新的 EnhancedComponent// EnhancedComponent1 !== EnhancedComponent2 const EnhancedComponent = enhance(MyComponent);// 这将导致子树每次渲染都会进行卸载,和重新挂载的操作! return <EnhancedComponent />;}
// 定义静态函数WrappedComponent.staticMethod = function() {/*...*/}// 现在使用 HOCconst EnhancedComponent = enhance(WrappedComponent);// 增强组件没有 staticMethodtypeof EnhancedComponent.staticMethod === 'undefined' // true
function enhance(WrappedComponent) { class Enhance extends React.Component {/*...*/}// 必须准确知道应该拷贝哪些方法 :( Enhance.staticMethod = WrappedComponent.staticMethod; return Enhance;}
import hoistNonReactStatic from 'hoist-non-react-statics';function enhance(WrappedComponent) { class Enhance extends React.Component {/*...*/} hoistNonReactStatic(Enhance, WrappedComponent); return Enhance;}
// 使用这种方式代替...MyComponent.someFunction = someFunction;export default MyComponent;// ...单独导出该方法...export { someFunction };// ...并在要使用的组件中,import 它们import MyComponent, { someFunction } from './MyComponent.js';
4、Refs 不会被传递
虽然高阶组件的约定是将所有 props 传递给被包装组件,但这对于 Refs 并不适用。那是因为 ref 实际上并不是一个 prop , 就像 key 一样,它是由 React 专门处理的。如果将 ref 添加到 HOC 的返回组件中,则 ref 引用指向容器组件,而不是被包装组件。
这个问题的解决方案是通过使用 React.forwardRef API(React 16.3 中引入)。
三、Render props
概念介绍
示例
具有 render props 的组件预期子组件是一个函数,它所做的就是把子组件当做函数调用,调用参数就是传入的 props,然后把返回结果渲染出来。
<Provider> {props => <List add={props.add} />}</Provider>
我们具体看下Provider组件是如何定义的。通过这段代码props.children(allProps),我们调用了传入的函数。
const Provider = (props) => {// 判断是否是女性用户 let isWoman = Math.random() > 0.5 ? true : false if (isWoman) { const allProps = { add: '高阶组件增加的属性', ...props } return props.children(allProps) } else { return <div>女士专用,男士无权浏览</div>; }}
好像 render props 能做的高阶组件也都能做到啊,而且高阶组件更容易理解,是否render props 没啥用呢?我们来看一下 render props 更强大的一个点:对于新增的 props 更加灵活。假设我们的 List 组件接受的是 plus 属性,ShoppingCart 组件接受的是 add 属性,我们可以直接这样写,无需变动 List 组件以及 Provider 本身。使用高阶组件达到相同效果就要复杂很多。
<Provider> {props => { const { add } = props return < List plus={add} /> }}</Provider><Provider> {props => <ShoppingCart add={props.add} />}</Provider>
const Provider = (props) => {// 判断是否是女性用户 let isWoman = Math.random() > 0.5 ? true : false if (isWoman) { const allProps = { add: '高阶组件增加的属性', ...props } return props.test(allProps) } else { return <div>女士专用,男士无权浏览</div>; }}const ExampleRenderProps = () => { return ( <div> <Provider test={props => <List add={props.add} />} /> <Provider test={props => <ShoppingCart add={props.add} />} /> </div> )}
模式所解决的问题
使用注意事项
将 Render Props 与 React.PureComponent 一起使用时要小心!如果你在 Provider 属性中创建函数,那么使用 render props 会抵消使用React.PureComponent 带来的优势。因为浅比较 props 的时候总会得到 false,并且在这种情况下每一个 render 对于 render props 将会生成一个新的值。
例如,继续我们之前使用的 <List> 组件,如果 List 继承自 React.PureComponent 而不是 React.Component,我们的例子看起来就像这样:
class ExampleRenderProps extends React.Component { render() { return ( <div> {/* 这是不好的! 每个渲染的 `test` prop的值将会是不同的。 */} <Provider test={props => <List add={props.add} />} /> </div> ) }}
class ExampleRenderProps extends React.Component { renderList=()=>{ return <List add={props.add} /> } render() { return ( <div> <Provider test={this.renderList} /> </div> ) }}
如果你无法静态定义 prop(例如,因为你需要关闭组件的 props 和/或 state),则 <List> 应该扩展自React.Component.
小结
React官方文档
React Component Patterns
React实战:设计模式和最佳实践
Presentational and Container Components