如何将实例变量从 React 组件传递到其 HOC?

Posted

技术标签:

【中文标题】如何将实例变量从 React 组件传递到其 HOC?【英文标题】:How to pass in an instance variable from a React component to its HOC? 【发布时间】:2018-10-28 07:33:01 【问题描述】:

我通常使用组件组合来重用 React 方式的逻辑。例如,这是一个关于如何将交互逻辑添加到组件的简化版本。在这种情况下,我会将CanvasElement 设为可选:

CanvasElement.js

import React,  Component  from 'react'
import Selectable from './Selectable'
import './CanvasElement.css'

export default class CanvasElement extends Component 
  constructor(props) 
    super(props)

    this.state = 
      selected: false
    

    this.interactionElRef = React.createRef()
  

  onSelected = (selected) => 
    this.setState( selected)
  

  render() 
    return (
      <Selectable
        iElRef=this.interactionElRef
        onSelected=this.onSelected>

        <div ref=this.interactionElRef className='canvas-element ' + (this.state.selected ? 'selected' : '')>
          Select me
        </div>

      </Selectable>
    )
  

Selectable.js

import  Component  from 'react'
import PropTypes from 'prop-types'

export default class Selectable extends Component 
  static propTypes = 
    iElRef: PropTypes.shape(
      current: PropTypes.instanceOf(Element)
    ).isRequired,
    onSelected: PropTypes.func.isRequired
  

  constructor(props) 
    super(props)

    this.state = 
      selected: false
    
  

  onClick = (e) => 
    const selected = !this.state.selected
    this.setState( selected )
    this.props.onSelected(selected)
  

  componentDidMount() 
    this.props.iElRef.current.addEventListener('click', this.onClick)
  

  componentWillUnmount() 
    this.props.iElRef.current.removeEventListener('click', this.onClick)
  

  render() 
    return this.props.children
  

效果很好。 Selectable 包装器不需要创建新的 div,因为它的父级为它提供了对另一个将变为可选择的元素的引用。

但是,我在很多场合都被建议停止使用这种 Wrapper 组合,而是通过 Higher Order Components 实现可重用性。愿意尝试 HoC,我尝试了一下,但没有比这更进一步:

CanvasElement.js

import React,  Component  from 'react'
import Selectable from '../enhancers/Selectable'
import flow from 'lodash.flow'
import './CanvasElement.css'

class CanvasElement extends Component 
  constructor(props) 
    super(props)

    this.interactionElRef = React.createRef()
  

  render() 
    return (
      <div ref=this.interactionElRef>
        Select me
      </div>
    )
  


export default flow(
  Selectable()
)(CanvasElement)

Selectable.js

import React,  Component  from 'react'

export default function makeSelectable() 
  return function decorateComponent(WrappedComponent) 
    return class Selectable extends Component 

      componentDidMount() 
        // attach to interaction element reference here
      

      render() 
        return (
          <WrappedComponent ...this.props />
        )
      
    
  

问题在于似乎没有明显的方法可以将增强组件的引用(实例变量)连接到高阶组件(增强器)。

我如何将实例变量(interactionElRef)从 CanvasElement “传递”到它的 HOC?

【问题讨论】:

【参考方案1】:

我想出了一个不同的策略。它的行为大致类似于 Redux connect 函数,提供包装组件不负责创建的 props,但子组件负责在他们认为合适的时候使用它们:

CanvasElement.js

import React,  Component  from "react";
import makeSelectable from "./Selectable";

class CanvasElement extends Component 
  constructor(props) 
    super(props);
  

  render() 
    const  onClick, selected  = this.props;
    return <div onClick=onClick>`Selected: $selected`</div>;
  


CanvasElement.propTypes = 
  onClick: PropTypes.func,
  selected: PropTypes.bool,
;

CanvasElement.defaultProps = 
  onClick: () => ,
  selected: false,
;

export default makeSelectable()(CanvasElement);

Selectable.js

import React,  Component  from "react";

export default makeSelectable = () => WrappedComponent => 
  const selectableFactory = React.createFactory(WrappedComponent);

  return class Selectable extends Component 
    state = 
      isSelected: false
    ;

    handleClick = () => 
      this.setState(
        isSelected: !this.state.isSelected
      );
    ;

    render() 
      return selectableFactory(
        ...this.props,
        onClick: this.handleClick,
        selected: this.state.isSelected
      );
    
  
;

https://codesandbox.io/s/7zwwxw5y41


我知道这并不能回答您的问题。我认为您试图让孩子在父母不知情的情况下逃脱。

不过,ref 路线感觉不对。我喜欢将工具与孩子联系起来的想法。您可以响应任一点击中的点击。

让我知道你的想法。

【讨论】:

恕我直言,如果 CanvasElement 始终是可选的,这是迄今为止最好的尝试。在这种情况下,HOC 仍然有意义,因为您仍然可以使其他组件始终可选。主要缺点是您的增强组件需要知道它正在被包装。有时间我也会试一试的。 这也是我认为的主要缺点。但是,所有 OP 的解决方案仍然具有该知识。 您能否详细说明为什么这比传入回调函数更好?我的 HoC 的主要目的是我不需要强制添加事件侦听器等。我只想声明元素是可选择的,而不必担心在正确的位置添加事件侦听器。 我们continue this in chat【参考方案2】:

我现在想出了一个自以为是的解决方案,其中 HoC 将两个回调函数注入到增强组件中,一个用于注册 dom 引用,另一个用于注册一个回调,当元素被选中或取消选中时调用:

makeElementSelectable.js

import React,  Component  from 'react'
import PropTypes from 'prop-types'
import movementIsStationary from '../lib/movement-is-stationary';

/*
  This enhancer injects the following props into your component:
  - setInteractableRef(node) - a function to register a React reference to the DOM element that should become selectable
  - registerOnToggleSelected(cb(bool)) - a function to register a callback that should be called once the element is selected or deselected
*/
export default function makeElementSelectable() 
  return function decorateComponent(WrappedComponent) 
    return class Selectable extends Component 
      static propTypes = 
        selectable: PropTypes.bool.isRequired,
        selected: PropTypes.bool
      

      eventsAdded = false

      state = 
        selected: this.props.selected || false,
        lastDownX: null,
        lastDownY: null
      

      setInteractableRef = (ref) => 
        this.ref = ref

        if (!this.eventsAdded && this.ref.current) 
          this.addEventListeners(this.ref.current)
        

        // other HoCs may set interactable references too
        this.props.setInteractableRef && this.props.setInteractableRef(ref)
      

      registerOnToggleSelected = (cb) => 
        this.onToggleSelected = cb
      

      componentDidMount() 
        if (!this.eventsAdded && this.ref && this.ref.current) 
          this.addEventListeners(this.ref.current)
        
      

      componentWillUnmount() 
        if (this.eventsAdded && this.ref && this.ref.current) 
          this.removeEventListeners(this.ref.current)
        
      

      /*
        keep track of where the mouse was last pressed down
      */
      onMouseDown = (e) => 
        const lastDownX = e.clientX
        const lastDownY = e.clientY

        this.setState(
          lastDownX, lastDownY
        )
      

      /*
        toggle selected if there was a stationary click
        only consider clicks on the exact element we are making interactable
      */
      onClick = (e) => 
        if (
          this.props.selectable
          && e.target === this.ref.current
          && movementIsStationary(this.state.lastDownX, this.state.lastDownY, e.clientX, e.clientY)
        ) 
          const selected = !this.state.selected
          this.onToggleSelected && this.onToggleSelected(selected, e)
          this.setState( selected )
        
      

      addEventListeners = (node) => 
        node.addEventListener('click', this.onClick)
        node.addEventListener('mousedown', this.onMouseDown)

        this.eventsAdded = true
      

      removeEventListeners = (node) => 
        node.removeEventListener('click', this.onClick)
        node.removeEventListener('mousedown', this.onMouseDown)

        this.eventsAdded = false
      

      render() 
        return (
          <WrappedComponent
            ...this.props
            setInteractableRef=this.setInteractableRef
            registerOnToggleSelected=this.registerOnToggleSelected />
        )
      
    
  

CanvasElement.js

import React,  PureComponent  from 'react'
import  connect  from 'react-redux'
import  bindActionCreators  from 'redux'
import PropTypes from 'prop-types'
import flowRight from 'lodash.flowright'
import  moveSelectedElements  from '../actions/canvas'
import makeElementSelectable from '../enhancers/makeElementSelectable'

class CanvasElement extends PureComponent 
  static propTypes = 
    setInteractableRef: PropTypes.func.isRequired,
    registerOnToggleSelected: PropTypes.func
  

  interactionRef = React.createRef()

  componentDidMount() 
    this.props.setInteractableRef(this.interactionRef)
    this.props.registerOnToggleSelected(this.onToggleSelected)
  

  onToggleSelected = async (selected) => 
    await this.props.selectElement(this.props.id, selected)
  

  render() 
    return (
      <div ref=this.interactionRef>
        Select me
      </div>
    )
  


const mapStateToProps = (state, ownProps) => 
  const 
    canvas: 
      selectedElements
    
   = state

  const selected = !!selectedElements[ownProps.id]

  return 
    selected
  


const mapDispatchToProps = dispatch => (
  selectElement: bindActionCreators(selectElement, dispatch)
)

const ComposedCanvasElement = flowRight(
  connect(mapStateToProps, mapDispatchToProps),
  makeElementSelectable()
)(CanvasElement)

export default ComposedCanvasElement

这可行,但我至少能想到一个重要问题:HoC 将 2 个 props 注入到增强组件中;但是增强的组件无法以声明方式定义注入哪些道具,只需要“相信”这些道具可以神奇地使用

不胜感激有关此方法的反馈/想法。也许有更好的方法,例如通过将“mapProps”对象传递给makeElementSelectable 来明确定义要注入的道具?

【讨论】:

【参考方案3】:

就像您在 CanvasElement 的 DOM 元素上所做的那样,Ref 也可以附加到类组件,请查看Adding a Ref to a Class Component 的文档

export default function makeSelectable() 
  return function decorateComponent(WrappedComponent) 
    return class Selectable extends Component 
      canvasElement = React.createRef()

      componentDidMount() 
        // attach to interaction element reference here
        console.log(this.canvasElement.current.interactionElRef)
      

      render() 
        return (
          <WrappedComponent ref=this.canvasElement ...this.props />
        )
      
    
  

此外,如果您需要渲染树中更高级别的祖先中的子实例引用,请查看Ref forwarding。所有这些解决方案都基于您使用 React 16.3+ 的假设。

一些注意事项:

在极少数情况下,您可能希望从父组件访问子组件的 DOM 节点。通常不建议这样做,因为它会破坏组件封装,但它有时对触发焦点或测量子 DOM 节点的大小或位置很有用。

虽然您可以向子组件添加 ref,但这不是一个理想的解决方案,因为您只会获得组件实例而不是 DOM 节点。此外,这不适用于功能组件。 https://reactjs.org/docs/forwarding-refs.html

【讨论】:

谢谢,但这似乎是一种不好的做法,因为 HoC 现在隐含地依赖于.interactionElRef。它不能重用,因为您不能在不使用此确切命名的其他组件中使用它。这不是为什么 redux 有 mapStateToProps 和 mapDispatchToProps 等吗?或者为什么 React DND 有收集? React 鼓励单向流,即数据通过 props 从 Parent 传递给 Children,而事件应该冒泡。使用 refs 显然您正在从父级访问子级的数据,正如您所说的那样创建耦合,这就是文档包含上述警告的原因。 mapStateToPropsmapDispatchToProps 是另一回事,它们是从 redux 存储计算派生数据的选择器,但仍然遵循单向流程,因为只要您的组件“已连接”,redux 存储是跨整个渲染树共享的全局数据"。 很少需要 Refs,但有时您确实希望访问浏览器 dom 元素而不是 react' vdom 元素,以像inputElement.focus() 那样直接操作 dom,大多数时候将子数据传递给父级应该由事件处理程序(回调)完成。 谢谢 - 遵循 React 的单向流程,您将如何解决我的问题,即以可重用的方式使元素可选?你会推荐 HoC 吗? 我检查了你的组合方法对我来说看起来相当不错,我看不出切换到 HoC 有任何明显的好处,只要需要 Ref,它无论如何都不会提高可重用性。 HoC 更像是一种封装/集中业务逻辑(身份验证/功能标志/分析等)以与渲染逻辑分离的组件方法,它也被很好地采用了带有许多陷阱的模式,因此 render props 作为更灵活的替代方案出现。

以上是关于如何将实例变量从 React 组件传递到其 HOC?的主要内容,如果未能解决你的问题,请参考以下文章

React TypeScript HoC - 将组件作为道具传递

Graphql,react-apollo如何在加载组件状态时将变量传递给查询

如何将儿童类型传递给 HOC 反应的道具

通过 HOC 将 React 上下文传递给包装的组件

React Redux 使用带有连接组件的 HOC

如何将字符串转换为 React 组件