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