React 的设计模式 https://levelup.gitconnected.com/react-component-patterns-ab1f09be2c82 有很多种, 比如无状态组件 / 表现型组件, 有状态组件 / 容器型组件, render 模式组件, 高阶组件等等. 本文主要介绍 react 的 render 模式与 HOC 设计模式, 并通过实际案例进行比较.
render props 模式
The Render Props 是一种在不重复代码的情况下共享组件间功能的方法. 如下所示:
- <DataProvider render={data => (
- <h1>Hello {data.target}</h1>
- )}/>
通过使用 prop 来定义呈现的内容, 组件只是注入功能, 而不需要知道它如何应用于 UI.render prop 模式意味着用户通过定义单独组件来传递 prop 方法, 来指示共享组件应该返回的内容.
Render Props 的核心思想是, 通过一个函数将 class 组件的 state 作为 props 传递给纯函数组件
- import React from 'react';
- const SharedComponent extends React.Component {
- state = {...}
- render() {
- return (
- <div>
- {this.props.render(this.state)}
- </div>
- );
- }
- }
- export default SharedComponent;
this.props.render() 是由另外一个组件传递过来的. 为了使用以上组件, 我们可以进行下面的操作:
- import React from 'react';
- import SharedComponent from 'components/SharedComponent';
- const SayHello = () => (
- <SharedComponent render={(state) => (
- <span>hello!,{...state}</span>
- )} />
- );
{this.props.render(this.state)} 这个函数, 将其 state 作为参数传入其的 props.render 方法中, 调用时直接取组件所需要的 state 即可.
render props 模式最重要的是它返回一个 react 元素, 比如我将上面的 render 属性改名, 依然有效.
- import React from 'react';
- const SharedComponentWithGoofyName extends React.Component {
- render() {
- return (
- <div>
- {this.props.wrapThisThingInADiv()}
- </div>
- );
- }
- }
- const SayHelloWithGoofyName = () => (
- <SharedComponentWithGoofyName wrapThisThingInADiv={() => (
- <span>hello!</span>
- )} />
- );
HOC 设计模式
React 的高阶组件主要用于组件之间共享通用功能而不重复代码的模式 (也就是达到 DRY 模式).
高阶组件实际是一个函数. HOC 函数将组件作为参数并返回一个新的组件. 它将组件转换为另一个组件并添加额外的数据或功能.
高阶组件在 React 生态链技术中经常用到, 对读者较为熟悉的, 比如 Redux 中的 connect,React Router 中的 withRouter 等.
常见的高阶组件如下所示:
- import React from 'react';
- const withSecretToLife = (WrappedComponent) => {
- class HOC extends React.Component {
- render() {
- return (
- <WrappedComponent
- secretToLife={42}
- {...this.props}
- />
- );
- }
- }
- return HOC;
- };
- export default withSecretToLife;
已知 secretToLife 为 42, 有一些组件需要共享这个信息, 此时创建了 SecretToLife 的 HOC, 将它作为 prop 传递给我们的组件.
- import React from 'react';
- import withSecretToLife from 'components/withSecretToLife';
- const DisplayTheSecret = props => (
- <div>
The secret to life is {props.secretToLife}.
- </div>
- );
- const WrappedComponent = withSecretToLife(DisplayTheSecret);
- export default WrappedComponent;
此时, WrappedComponent 只是 DisplayTheSecret 的增强版本, 允许我们访问 secretToLife 属性.
Render Props 与 HOC 模式实例对比
本文以一个利用 localStorage API 的小例子分别使用 HOC 设计模式跟 The Render Props 设计模式编写 demo.
- HOC Example
- import React from 'react';
- const withStorage = (WrappedComponent) => {
- class HOC extends React.Component {
- state = {
- localStorageAvailable: false,
- };
- componentDidMount() {
- this.checkLocalStorageExists();
- }
- checkLocalStorageExists() {
- const testKey = 'test';
- try {
- localStorage.setItem(testKey, testKey);
- localStorage.removeItem(testKey);
- this.setState({ localStorageAvailable: true });
- } catch(e) {
- this.setState({ localStorageAvailable: false });
- }
- }
- load = (key) => {
- if (this.state.localStorageAvailable) {
- return localStorage.getItem(key);
- }
- return null;
- }
- save = (key, data) => {
- if (this.state.localStorageAvailable) {
- localStorage.setItem(key, data);
- }
- }
- remove = (key) => {
- if (this.state.localStorageAvailable) {
- localStorage.removeItem(key);
- }
- }
- render() {
- return (
- <WrappedComponent
- load={this.load}
- save={this.save}
- remove={this.remove}
- {...this.props}
- />
- );
- }
- }
- return HOC;
- }
- export default withStorage;
在 withStorage 中, 使用 componentDidMount 生命周期函数来检查 checkLocalStorageExists 函数中是否存在 localStorage.
local,save,remove 则是来操作 localStorage 的. 现在我们创建一个新的组件, 将其包裹在 HOC 组件中, 用于显示相关的信息. 由于获取信息的 API 调用需要很长时间, 我们可以假设这些值一旦设定就不会改变. 我们只会在未保存值的情况下进行此 API 调用. 然后, 每当用户返回页面时, 他们都可以立即访问数据, 而不是等待我们的 API 返回.
- import React from 'react';
- import withStorage from 'components/withStorage';
- class ComponentNeedingStorage extends React.Component {
- state = {
- username: '',
- favoriteMovie: '',
- }
- componentDidMount() {
- const username = this.props.load('username');
- const favoriteMovie = this.props.load('favoriteMovie');
- if (!username || !favoriteMovie) {
- // This will come from the parent component
- // and would be passed when we spread props {...this.props}
- this.props.reallyLongApiCall()
- .then((user) => {
- this.props.save('username', user.username) || '';
- this.props.save('favoriteMovie', user.favoriteMovie) || '';
- this.setState({
- username: user.username,
- favoriteMovie: user.favoriteMovie,
- });
- });
- } else {
- this.setState({ username, favoriteMovie })
- }
- }
- render() {
- const { username, favoriteMovie } = this.state;
- if (!username || !favoriteMovie) {
- return <div>Loading...</div>;
- }
- return (
- <div>
My username is {username}, and I love to watch {favoriteMovie}.
- </div>
- )
- }
- }
- const WrappedComponent = withStorage(ComponentNeedingStorage);
- export default WrappedComponent;
在封装组件的 componentDidMount 内部, 首先尝试从 localStorage 中获取, 如果不存在, 则异步调用, 将获得的信息存储到 localStorage 并显示出来.
The Render Props Exapmle
- import React from 'react';
- class Storage extends React.Component {
- state = {
- localStorageAvailable: false,
- };
- componentDidMount() {
- this.checkLocalStorageExists();
- }
- checkLocalStorageExists() {
- const testKey = 'test';
- try {
- localStorage.setItem(testKey, testKey);
- localStorage.removeItem(testKey);
- this.setState({ localStorageAvailable: true });
- } catch(e) {
- this.setState({ localStorageAvailable: false });
- }
- }
- load = (key) => {
- if (this.state.localStorageAvailable) {
- return localStorage.getItem(key);
- }
- return null;
- }
- save = (key, data) => {
- if (this.state.localStorageAvailable) {
- localStorage.setItem(key, data);
- }
- }
- remove = (key) => {
- if (this.state.localStorageAvailable) {
- localStorage.removeItem(key);
- }
- }
- render() {
- return (
- <span>
- this.props.render({
- load: this.load,
- save: this.save,
- remove: this.remove,
- })
- </span>
- );
- }
- }
Storage 组件内部与 HOC 的 withStorage 较为类似, 不同的是 Storage 不接受组件为参数, 并且返回 this.props.render.
- import React from 'react';
- import Storage from 'components/Storage';
- class ComponentNeedingStorage extends React.Component {
- state = {
- username: '',
- favoriteMovie: '',
- isFetching: false,
- }
- fetchData = (save) => {
- this.setState({ isFetching: true });
- this.props.reallyLongApiCall()
- .then((user) => {
- save('username', user.username);
- save('favoriteMovie', user.favoriteMovie);
- this.setState({
- username: user.username,
- favoriteMovie: user.favoriteMovie,
- isFetching: false,
- });
- });
- }
- render() {
- return (
- <Storage
- render={({ load, save, remove }) => {
- const username = load('username') || this.state.username;
- const favoriteMovie = load('favoriteMovie') || this.state.username;
- if (!username || !favoriteMovie) {
- if (!this.state.isFetching) {
- this.fetchData(save);
- }
- return <div>Loading...</div>;
- }
- return (
- <div>
My username is {username}, and I love to watch {favoriteMovie}.
- </div>
- );
- }}
- />
- )
- }
- }
对于 ComponentNeedingStorage 组件来说, 利用了 Storage 组件的 render 属性传递的三个方法, 进行一系列的数据操作, 从而展示相关的信息.
render props VS HOC 模式
总的来说, render props 其实和高阶组件类似, 就是在 puru component 上增加 state, 响应 react 的生命周期.
对于 HOC 模式来说, 优点如下:
支持 ES6
复用性强, HOC 为纯函数且返回值为组件, 可以多层嵌套
支持传入多个参数, 增强了适用范围
当然也存在如下缺点:
当多个 HOC 一起使用时, 无法直接判断子组件的 props 是哪个 HOC 负责传递的
多个组件嵌套, 容易产生同样名称的 props
HOC 可能会产生许多无用的组件, 加深了组件的层级
Render Props 模式的出现主要是为了解决 HOC 所出现的问题. 优点如下所示:
支持 ES6
不用担心 props 命名问题, 在 render 函数中只取需要的 state
不会产生无用的组件加深层级
render props 模式的构建都是动态的, 所有的改变都在 render 中触发, 可以更好的利用组件内的生命周期.
当然笔者认为, 对于 Render Props 与 HOC 两者的选择, 应该根据不同的场景进行选择. Render Props 模式比 HOC 更直观也更利于调试, 而 HOC 可传入多个参数, 能减少不少的代码量.
Render Props 对于只读操作非常适用, 如跟踪屏幕上的滚动位置或鼠标位置. HOC 倾向于更好地执行更复杂的操作, 例如以上的 localStorage 功能.
参考文献
Understanding React Render Props by Example https://levelup.gitconnected.com/understanding-react-render-props-by-example-71f2162fd0f2
- Understanding React Higher-Order Components by Example https://levelup.gitconnected.com/understanding-react-higher-order-components-by-example-95e8c47c8006
- Ultimate React Component Patterns with Typescript 2.8 https://levelup.gitconnected.com/ultimate-react-component-patterns-with-typescript-2-8-82990c516935
- React Component Patterns https://levelup.gitconnected.com/react-component-patterns-ab1f09be2c82
来源: http://www.jianshu.com/p/ff6b3008820a