Как добиться повторного использования компонентов с помощью React и распространения событий мыши?

рассмотрим следующую типичную структуру документа React:

компонент.jsx

<OuterClickableArea>
    <InnerClickableArea>
        content
    </InnerClickableArea>
</OuterClickableArea>

где эти компоненты составлены следующим образом:

OuterClickableArea.js

export default class OuterClickableArea extends React.Component {
    constructor(props) {
        super(props)

        this.state = {
            clicking: false
        }

        this.onMouseDown = this.onMouseDown.bind(this)
        this.onMouseUp = this.onMouseUp.bind(this)
    }

    onMouseDown() {
        if (!this.state.clicking) {
            this.setState({ clicking: true })
        }
    }

    onMouseUp() {
        if (this.state.clicking) {
            this.setState({ clicking: false })
            this.props.onClick && this.props.onClick.apply(this, arguments)
            console.log('outer area click')
        }
    }

    render() {
        return (
            <div
                onMouseDown={this.onMouseDown}
                onMouseUp={this.onMouseUp}
            >
                { this.props.children }
            </div>
        )
    }
}

С, ради этого аргумента, InnerClickableArea.js, имеющий почти тот же код, за исключением имени класса и оператора журнала консоли.

теперь, если вы запустите это приложение, и пользователь нажмите на внутреннюю область кликабельности, произойдет следующее (Как и ожидалось):

  1. внешняя область регистрирует обработчики событий мыши
  2. внутренняя область регистрирует обработчики событий мыши
  3. пользователь нажимает мышь вниз во внутренней области
  4. триггеры прослушивателя mouseDown внутренней области
  5. прослушиватель mouseDown внешней области запускает
  6. пользователь выпускает мышь вверх
  7. прослушиватель mouseUp внутренней области триггеры
  8. журналы консоли "внутренняя область нажмите"
  9. запускает прослушиватель mouseUp внешней области
  10. журналы консоли "внешняя область нажмите"

это отображает типичное событие пузырящееся / распространение - никаких сюрпризов до сих пор.

он выведет:

inner area click
outer area click

теперь, что делать, если мы создаем приложение, где только одно взаимодействие может произойти в то время? Например, представьте себе редактор, в котором элементы могут быть выбраны с помощью мыши щелчки. При нажатии внутри внутренней области, мы хотели бы только выбрать внутренний элемент.

простым и очевидным решением было бы добавить stopPropagation внутри компонента InnerArea:

InnerClickableArea.js

    onMouseDown(e) {
        if (!this.state.clicking) {
            e.stopPropagation()
            this.setState({ clicking: true })
        }
    }

это работает, как ожидалось. Он выведет:

inner area click

проблема?

InnerClickableArea настоящим безоговорочно выбрал для OuterClickableArea (и все другие родители) не быть возможность получения события. Хотя InnerClickableArea не должен знать о существовании OuterClickableArea. Прямо как OuterClickableArea не знаю, о InnerClickableArea (после разделения проблем и концепций повторного использования). Мы создали неявную зависимость между двумя компонентами, где OuterClickableArea "знает", что он не будет ошибочно уволен своих слушателей, потому что он" помнит", что InnerClickableArea остановит любое распространение событий. Это кажется неправильным.

я пытаюсь не использовать stopPropagation, чтобы сделать приложение более масштабируемым, потому что было бы проще добавлять новые функции, которые полагаются на события без необходимости помнить, какие компоненты используют stopPropagation в какое время.

вместо этого я хотел бы сделать приведенную выше логику более декларативной, например, определив декларативно внутри Component.jsx является ли каждая из областей в настоящее время кликабельной или нет. Я бы сделал это состояние приложения известно, передавая, являются ли области кликабельными или нет, и переименовать его в Container.jsx. Что-то вот так:

контейнер.jsx

<OuterClickableArea
    clickable={!this.state.interactionBusy}
    onClicking={this.props.setInteractionBusy.bind(this, true)} />

    <InnerClickableArea
        clickable={!this.state.interactionBusy}
        onClicking={this.props.setInteractionBusy.bind(this, true)} />

            content

    </InnerClickableArea>

</OuterClickableArea>

здесь this.props.setInteractionBusy является действием redux, которое приведет к обновлению состояния приложения. Кроме того, этот контейнер будет иметь состояние приложения, сопоставленное с его реквизитом (не показано выше), поэтому this.state.ineractionBusy будут определены.

OuterClickableArea.js (почти то же самое для InnerClickableArea.в JS)

export default class OuterClickableArea extends React.Component {
    constructor(props) {
        super(props)

        this.state = {
            clicking: false
        }

        this.onMouseDown = this.onMouseDown.bind(this)
        this.onMouseUp = this.onMouseUp.bind(this)
    }

    onMouseDown() {
        if (this.props.clickable && !this.state.clicking) {
            this.setState({ clicking: true })
            this.props.onClicking && this.props.onClicking.apply(this, arguments)
        }
    }

    onMouseUp() {
        if (this.state.clicking) {
            this.setState({ clicking: false })
            this.props.onClick && this.props.onClick.apply(this, arguments)
            console.log('outer area click')
        }
    }

    render() {
        return (
            <div
                onMouseDown={this.onMouseDown}
                onMouseUp={this.onMouseUp}
            >
                { this.props.children }
            </div>
        )
    }
}

проблема в том, что цикл событий Javascript, похоже, запускает эти операции в следующий порядок:

  1. и OuterClickableArea и InnerClickableArea создаются с помощью prop clickable равна true (потому что состояние приложения interactionBusy по умолчанию false)
  2. внешняя область регистрирует обработчики событий мыши
  3. внутренняя область регистрирует обработчики событий мыши
  4. пользователь нажимает мышь вниз во внутренней области
  5. триггеры прослушивателя mouseDown внутренней области
  6. onClicking внутренней области уволен
  7. контейнер запускает действие "setInteractionBusy"
  8. redux состояние приложения interactionBusy установлено значение true
  9. триггеры прослушивателя mouseDown внешней области (это clickable опора все еще true потому что, хотя состояние приложения interactionBusy и true, react еще не вызвал повторного рендеринга; операция рендеринга помещается в конец цикла событий Javascript)
  10. пользователь выпускает мышь вверх
  11. триггеры прослушивателя mouseUp внутренней области
  12. журналы "внутренний нажмите"
  13. запускает прослушиватель mouseUp внешней области
  14. журналы консоли "внешняя область нажмите"
  15. react повторно отображает Component.jsx и передает clickable as false для обоих компонентов, но теперь это слишком поздно!--52-->

это выведет:

inner area click
outer area click

тогда как желаемый результат:

inner area click

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

поэтому мой вопрос:: есть ли альтернатива использованию состояния приложения, чтобы иметь возможность работать с распространением событий мыши декларативным образом, или есть лучший способ реализовать/выполнить мою выше установку с состоянием приложения (redux)?

5 ответов


самая простая альтернатива этому-добавить флаг перед запуском stopPropogation, и флаг в этом случае является аргументом.

const onMouseDown = (stopPropagation) => {
stopPropagation && event.stopPropagation();
}

теперь даже состояние приложения или опора может даже решить вызвать stoppropagation

<div onMouseDown={this.onMouseDown(this.state.stopPropagation)} onMouseUp={this.onMouseUp(this.props.stopPropagation)} >
    <InnerComponent stopPropagation = {true}>
</div>

как упоминал @Rikin и как вы упомянули в своем ответе, событие.stopPropagation решит вашу проблему в ее нынешнем виде.

I'm trying not to use stopPropagation to make the app more scalable, because it would be easier to add new features that rely on events without having to remember which components use stopPropagation at which time.

не является ли это преждевременной проблемой с точки зрения текущей области вашей компонентной архитектуры? Если вы хотите innerComponent быть вложенным в outerClickableArea единственный способ достичь этого-остановить или как-то определить, какие события игнорировать. Лично я бы прекратил распространение ради всех событий, и ради тех, щелкните события, для которых вы хотите выборочно разделить состояние между компонентами, запишите события и отправьте действие (при использовании redux) в глобальное состояние. Таким образом, вам не нужно беспокоиться о выборочном игнорировании/захвате пузырчатых событий и может обновить глобальный источник истины для тех событий, которые подразумевают общее состояние


вместо обработки события click и clickable состояние в компоненте, используйте LOCK состояние в состояние Redux и когда пользователь нажимает, компоненты просто испускают CLICK действие, удерживайте замок и выполните логику. Вот так:

  1. и OuterClickableArea и InnerClickableArea созданы
  2. Redux состояние с полем CLICK_LOCK по умолчанию false.
  3. внешняя область регистрирует обработчики событий мыши
  4. внутренняя область регистрирует мышь обработчики событий
  5. пользователь нажмите во внутренней области
  6. внутренняя область mouseDown слушатель триггеры
  7. внутренняя область испускает действие INNER_MOUSEDOWN
  8. внешняя область mouseDown слушатель триггеры
  9. внешняя область испускает действие OUTER_MOUSEDOWN
  10. INNER_MOUSEDOWN действие установить CLICK_LOCK государство true и сделать логику (log inner area click).
  11. , потому что CLICK_LOCK is true, OUTER_MOUSEDOWN ничего не делать.
  12. внутренняя область mouseUp слушатель триггеры
  13. внутренняя область испускает действие INNER_MOUSEUP
  14. внешняя область mouseUp слушатель триггеры
  15. внешняя область испускает действие OUTER_MOUSEUP
  16. INNER_MOUSEUP действие установить CLICK_LOCK государство false.
  17. OUTER_MOUSEUP действие установить CLICK_LOCK государство false.
  18. ничего не нужно повторно визуализировать, если логика щелчка не изменит некоторое состояние.

Примечание:

  • этот подход к работе, имея блокировку в регионе, в одно время может только один компонент click. Каждый компонент не должен знать о других, но для оркестровки требуется глобальное состояние (Redux).
  • если скорость нажатия высока, эта производительность подхода может быть плохой.
  • В настоящее время Redux действия гарантированный быть испущенным одновременно. Не уверен насчет будущего.

Я подозреваю, что вы собираетесь создать что-то вроде наложения модального, где будет компонент, который должен быть закрыт, когда был нажат внешний компонент или компонент контейнера, если это так, вы можете использовать react-overlays вместо того, чтобы воссоздавать все это, вы также можете узнать, как они обрабатывают этот вид реализации, если у вас есть другая цель.


вы хотя бы передаете информацию, если событие уже было обработано в самом событии?

const handleClick = e => {
    if (!e.nativeEvent.wasProcessed) {
      // do something
      e.nativeEvent.wasProcessed = true;
    }
  };

таким образом, внешний компонент может проверить, что событие уже обработано одним из его дочерних элементов, в то время как событие все еще будет пузыриться.

Я разместил образец, который содержит два кликабельных компонента. Событие mousedown обрабатывается только внутренняя кликабельно компонента пользователь нажал на: https://codesandbox.io/s/2wmxxzormy

отказ от ответственности: я не мог найти никакой информации об этом именно плохой практике, но в целом изменение объекта, которым вы не владеете, может привести, например, к коллизиям имен в будущем.