Как передать переменную экземпляра из компонента React в его HOC? - PullRequest
0 голосов
/ 18 мая 2018

Обычно я использую композицию компонентов для повторного использования логики способом 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.и вместо этого достигните повторного использования через Компоненты высшего порядка .Желая поэкспериментировать с 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?

Ответы [ 3 ]

0 голосов
/ 20 мая 2018

Теперь я пришел к самоуверенному решению, в котором 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 подпорки в расширенный компонент;но расширенный компонент не имеет способа декларативно определить, какие реквизиты вводятся, и ему просто нужно «поверить», что эти реквизиты магически доступны

Буду признателен за отзывы / мысли об этом подходе.Возможно, есть лучший способ, например, передать объект "mapProps" в makeElementSelectable, чтобы явно определить, какие реквизиты вводятся?

0 голосов
/ 24 мая 2018

Я придумал другую стратегию.Он действует примерно так же, как функция Redux connect, предоставляя реквизиты, за которые упакованный компонент не отвечает за создание, но дочерний отвечает за их использование по своему усмотрению:

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 кажется неправильным.Мне нравится идея подключения инструментов к ребенку.Вы можете ответить на щелчок одним из них.

Дайте мне знать, что вы думаете.

0 голосов
/ 18 мая 2018

Как и в случае с элементом DOM для CanvasElement, Ref также может быть присоединен к компоненту класса, проверьте документ для Добавление ссылки на компонент класса

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} />
        )
      }
    }
  }
}

Кроме того, выполните оформление заказа Переадресация ссылки , если вам нужна ссылка на дочерний экземпляр в предках, которые на несколько уровней выше в дереве визуализации.Все эти решения основаны на предположениях о том, что вы реагируете 16.3 +.

Некоторые предостережения:

В редких случаях вам может потребоваться доступ к дочернему DOM-узлу изродительский компонент.Как правило, это не рекомендуется, поскольку это нарушает инкапсуляцию компонента, но иногда может быть полезно для запуска фокуса или измерения размера или положения дочернего узла DOM.

Хотя вы можете добавить ссылку на дочерний компонент, этоне является идеальным решением, так как вы получите только экземпляр компонента, а не узел DOM.Кроме того, это не будет работать с функциональными компонентами.https://reactjs.org/docs/forwarding-refs.html

...