Как добиться повторного использования компонентов с React и распространением событий мыши? - PullRequest
0 голосов
/ 11 мая 2018

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

Component.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. журналы консоли"external area click"

Показывает типичное всплытие / распространение событий - пока никаких сюрпризов.

Вывод:

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 и в какомtime.

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

Container.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 - избыточное действие, которое приведет к обновлению состояния приложения.Кроме того, в этом контейнере состояние приложения будет сопоставлено с его параметрами (не показано выше), поэтому будет определено 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 (поскольку по умолчанию в состоянии приложения applicationBusy установлено значение false)
  2. во внешней области регистрируются обработчики событий мыши
  3. во внутренней области регистрируются обработчики событий мыши
  4. пользователь нажимает кнопку мыши ввнутренняя область
  5. срабатывает прослушиватель mouseDown внутренней области
  6. срабатывает onClicking внутренней области
  7. контейнер выполняет действие setInteractionBusy
  8. избыточное состояние приложения interactionBusy isустановлен в true
  9. срабатывает прослушиватель mouseDown внешней области (это clickable prop по-прежнему true, потому что, хотя состояние приложения interactionBusy равно true, реакция еще не вызвала повторную визуализацию;операция рендеринга помещается в конец цикла событий Javascript)
  10. пользователь отпускает мышь вверх
  11. триггеры прослушивания mouseUp внутренней области
  12. консольные журналы "щелчок внутренней области"
  13. триггеры прослушивания mouseUp внешней области
  14. журналы консоли"external area click"
  15. реагирует повторно отображает Component.jsx и передает clickable как false обоим компонентам, но сейчас это слишком поздно

Это будетoutput:

inner area click
outer area click

Принимая во внимание, что желаемый вывод:

inner area click

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

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

Ответы [ 5 ]

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

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

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

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

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

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

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

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

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

Вместо обработки события 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 и выполняет логику (журналinner area click).
  11. Поскольку CLICK_LOCK равен true, OUTER_MOUSEDOWN ничего не делать.
  12. mouseUp прослушиватель во внутренней области
  13. Внутренняя область вызывает действие INNER_MOUSEUP
  14. Внешние области mouseUp триггеры слушателя
  15. Внешняя область испускает действие OUTER_MOUSEUP
  16. INNER_MOUSEUP Действие устанавливает состояние CLICK_LOCK на false. *Действие 1057 *
  17. OUTER_MOUSEUP устанавливает состояние CLICK_LOCK на false.
  18. Ничего не нужно перерисовывать, если только логика щелчка не изменилась.ome state.

Примечание:

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

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

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

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

<div onMouseDown={this.onMouseDown(this.state.stopPropagation)} onMouseUp={this.onMouseUp(this.props.stopPropagation)} >
    <InnerComponent stopPropagation = {true}>
</div>
0 голосов
/ 11 мая 2018

Как упоминал @Rikin и как вы упоминали в своем ответе, event.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, единственный способ добиться этого - остановитьPropagation или каким-либо образом определить, какие события игнорировать. Лично я бы остановил распространение для всех событий, а также для тех событий щелчка, для которых вы выборочно хотите поделиться состоянием между компонентами, захватить события и отправить действие (если используется избыточность) в глобальное состояние. Таким образом, вам не нужно беспокоиться о выборочном игнорировании / захвате всплывающих событий и вы можете обновить глобальный источник правды для тех событий, которые подразумевают общее состояние

...