【高级React技术】当企业级项目采用Refs 转发和使用 HOC 解决横切关注点问题

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第9天,点击查看活动详情

Refs 转发

引用转发是一种通过组件将引用自动传递到其子组件之一的技术。大多数应用程序中的组件通常不需要这样做。但它对某些组件非常有用,尤其是可重用的组件库。

function FancyButton(props) {
  return (
    <button className="FancyButton">
      {props.children}
    </button>
  );
}

React组件隐藏其实现细节,包括其渲染结果。使用FancyButton的其他组件通常不需要获取内部DOM元素按钮的ref。这很好,因为它可以防止组件过度依赖其他组件的DOM结构。

const FancyButton = React.forwardRef((props, ref) => (
  <button ref={ref} className="FancyButton">
    {props.children}
  </button>
));

// 你可以直接获取 DOM button 的 ref:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;

虽然这种封装对于应用程序级组件(如FeedStory或Comment)非常理想,但对于高度可重用的“叶”组件(如FancyButton或MyTextInput)来说可能不太方便。这些组件倾向于以类似于传统DOM按钮和输入的方式在整个应用程序中使用,访问它们的DOM节点对于管理焦点、选择或动画是不可避免的。

function logProps(WrappedComponent) {
  class LogProps extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('old props:', prevProps);
      console.log('new props:', this.props);
    }

    render() {
      return <WrappedComponent {...this.props} />;
    }
  }

  return LogProps;
}

我们调用React CreateRef创建一个React ref并将其分配给ref变量。
通过将ref指定为JSX属性,我们将其传递给<FancyButton ref={ref}>。
React将ref传递给forwardRef中的函数(props,ref)=>…作为其第二个参数。
我们将ref参数向下转发到<button ref={ref}>,并将其指定为JSX属性。
安装ref时,ref当前将指向<button>DOM节点。

class FancyButton extends React.Component {
  focus() {
    // ...
  }

  // ...
}

// 我们导出 LogProps,而不是 FancyButton。
// 虽然它也会渲染一个 FancyButton。
export default logProps(FancyButton);

幸运的是,我们可以使用React。forwardRef API显式地将引用转发到内部FancyButton组件。反应ForwardRef接受接收props和ref参数的渲染函数,并返回React节点。

function logProps(Component) {
  class LogProps extends React.Component {
    // ...
  }

  function forwardRef(props, ref) {
    return <LogProps {...props} forwardedRef={ref} />;
  }

  // 在 DevTools 中为该组件提供一个更有用的显示名。
  // 例如 “ForwardRef(logProps(MyComponent))”
  const name = Component.displayName || Component.name;
  forwardRef.displayName = `logProps(${name})`;

  return React.forwardRef(forwardRef);
}

需要返回多个 元素以使渲染的 HTML 有效。如果在的 render() 中使用了父 div,则生成的 HTML 将无效。

const EnhancedComponent = higherOrderComponent(WrappedComponent);

高阶组件(HOC)是React中重用组件逻辑的高级技术。HOC本身不是React API的一部分。这是一种基于React综合特性的设计模式。

class CommentList extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {
      // 假设 "DataSource" 是个全局范围内的数据源变量
      comments: DataSource.getComments()
    };
  }

  componentDidMount() {
    // 订阅更改
    DataSource.addChangeListener(this.handleChange);
  }

  componentWillUnmount() {
    // 清除订阅
    DataSource.removeChangeListener(this.handleChange);
  }

  handleChange() {
    // 当数据源更新时,更新组件状态
    this.setState({
      comments: DataSource.getComments()
    });
  }

  render() {
    return (
      <div>
        {this.state.comments.map((comment) => (
          <Comment comment={comment} key={comment.id} />
        ))}
      </div>
    );
  }
}

CommentList和BlogPost是不同的-它们在DataSource上调用不同的方法并呈现不同的结果。但它们的大多数实现都是相同的:
装载时,向DataSource添加更改侦听器。
在侦听器内部,当数据源发生更改时,会调用setState。
卸载时,删除侦听器。
可以想象,在大型应用程序中,这种订阅DataSource并调用setState的模式会反复出现。我们需要一个抽象,它允许我们在一个地方定义这个逻辑,并在多个组件之间共享它。这就是高阶组件的优势所在。
对于订阅了DataSource的组件,例如CommentList和BlogPost,我们可以编写一个创建组件函数。此函数将接受子组件作为其参数之一,子组件将使用订阅数据作为属性。让我们使用Subscription调用函数:

const CommentListWithSubscription = withSubscription(
  CommentList,
  (DataSource) => DataSource.getComments()
);

const BlogPostWithSubscription = withSubscription(
  BlogPost,
  (DataSource, props) => DataSource.getBlogPost(props.id)
);

使用 HOC 解决横切关注点问题

由于withSubscription是一个通用函数,因此您可以根据需要添加或删除参数。例如,您可能希望使数据属性的名称可配置,以进一步将HOC与包装器组件隔离开来。或者您可以接受用于配置shouldComponentUpdate的参数,或用于配置数据源的参数。因为HOC可以控制组件的定义方式,所以这一切都成为可能。
与组件一样,withSubscription和包装器组件之间的契约完全基于它们之间传递的属性。这种依赖模式可以很容易地替换HOC,只要它们为封装组件提供相同的支持。例如,当您需要使用其他数据库获取数据时,这很有用。

function logProps(InputComponent) {
  InputComponent.prototype.componentDidUpdate = function(prevProps) {
    console.log('Current props: ', this.props);
    console.log('Previous props: ', prevProps);
  };
  
  return InputComponent;
}

const EnhancedComponent = logProps(InputComponent);

每次调用 logProps 时,增强组件都会有 log 输出。返回原始的 input 组件,暗示它已经被修改。

const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent))

// ... 你可以编写组合工具函数
// compose(f, g, h) 等同于 (...args) => f(g(h(...args)))
const enhance = compose(
  // 这些都是单参数的 HOC
  withRouter,
  connect(commentSelector)
)
const EnhancedComponent = enhance(WrappedComponent)

这种形式可能看起来很混乱或不必要,但它有一个有用的属性。与connect函数返回的单个参数HOC一样,它具有签名Component=>Component。具有相同输出类型和输入类型的函数可以很容易地组合。

© 版权声明
THE END
喜欢就支持一下吧
点赞7 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容