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?

10
demandé sur Tom 2018-05-18 01:44:55

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.

6
répondu Reed Dunkle 2018-05-25 01:06:05
Ref peut être attaché à la composante de classe aussi bien, vérifiez le doc pour ajouter une Réf à une composante de classe

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

2
répondu Xlee 2018-05-18 03:28:26

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?

1
répondu Tom 2018-05-19 23:19:04