Comment passer dans une variable d'instance d'un composant React à son HOC?
j'utilise typiquement la composition des composants pour réutiliser la logique de la manière React. Par exemple, voici une version simplifiée sur la façon dont j'ajouterais la logique d'interaction à un composant. Dans ce cas, je voudrais faire CanvasElement
sélectionnable:
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>
)
}
}
Sélectionnable.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
}
}
Fonctionne assez bien. Le wrapper sélectionnable n'a pas besoin de créer un nouveau div parce que son parent lui fournit une référence à un autre élément c'est de devenir sélectionnable.
cependant, on m'a recommandé à de nombreuses reprises d'arrêter d'utiliser une telle composition D'enrubannage et de parvenir à la réutilisation par Composants D'Ordre Supérieur. Voulant expérimenter avec les HoCs, je lui ai donné un essai mais n'est pas allé plus loin que ceci:
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)
Sélectionnable.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} />
)
}
}
}
}
Le problème est qu'il semble y avoir aucun moyen évident pour connecter la référence du composant amélioré (une variable d'instance) au composant d'ordre supérieur (l'amplificateur).
Comment "passer" la variable d'instance (le interactionElRef
) de l'CanvasElement à ses HOC?
3 réponses
j'ai trouvé une stratégie différente. Il agit à peu près comme le Redux connect
fonction, fournissant des accessoires que le composant enveloppé n'est pas responsable de créer, mais l'enfant est responsable de les utiliser comme bon lui semble:
Canvasellement.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);
Sélectionnable.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
je sais cela ne veut pas répondre à votre question. Je pense que vous essayez de laisser l'enfant s'en tirer sans que le parent soit au courant.
ref
la route semble mauvaise, cependant. J'aime l'idée de relier les outils à l'enfant. Vous pouvez répondre au cliquez dans l'un des deux.
dites-moi ce que vous en pensez.
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} />
)
}
}
}
}
aussi, faire la caisse Ref redirection si vous avez besoin d'une référence d'instance d'enfant dans les ancêtres, c'est plusieurs niveaux plus haut dans l'arbre de rendu. Toutes ces solutions sont basées sur des hypothèses que vous êtes à réagir 16.3+.
Quelques mises en garde:
Dans dans de rares cas, vous pourriez vouloir avoir accès au noeud DOM d'un enfant à partir d'un composant parent. Cela n'est généralement pas recommandé car cela brise l'encapsulation des composants, mais cela peut parfois être utile pour déclencher la mise au point ou mesurer la taille ou la position d'un noeud DOM enfant.
alors que vous pouvez ajouter un ref au composant enfant, ce n'est pas une solution idéale, car vous n'obtiendriez qu'une instance component plutôt qu'un noeud DOM. En outre, cela ne fonctionnerait pas avec fonctionnel composant. https://reactjs.org/docs/forwarding-refs.html
j'ai maintenant trouvé une solution opiniâtre où le HoC injecte deux fonctions de callback dans le composant amélioré, l'une pour enregistrer la référence dom et l'autre pour enregistrer un callback qui est appelé lorsqu'un élément est sélectionné ou désélectionné:
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
cela fonctionne, mais je peux penser à au moins une question importante: le HoC injecte 2 accessoires dans le composante améliorée; mais la composante améliorée n'a aucun moyen de définir déclarativement quels accessoires sont injectés et a juste besoin de "croire" que ces accessoires sont magiquement disponibles
apprécierait des commentaires / réflexions sur cette approche. Peut-être y a-t-il un meilleur moyen, par exemple en passant dans un objet "mapProps" à makeElementSelectable
définir explicitement quels accessoires sont injectés?