Рассмотрим следующую типичную структуру документа 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 имеет практически одинаковый код, за исключением имени класса и оператора журнала консоли.
Теперь, если вы запустите это приложение, и пользователь нажмет на внутреннюю активируемую область, произойдет следующее (как и ожидалось):
- во внешней области регистрируются обработчики событий мыши
- во внутренней области регистрируются обработчики событий мыши
- пользователь нажимает кнопку мыши во внутренней области
- прослушиватель mouseDown внутренней области запускает
- прослушиватель mouseDown внешней области запускает
- пользователяосвобождает мышь
- триггеры прослушивателя mouseUp во внутренней области
- консольные журналы "щелчок внутренней области"
- триггеры прослушивателя mouseUp внешней области
- журналы консоли"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, по-видимому, выполняет эти операции в следующем порядке:
- и
OuterClickableArea
, и InnerClickableArea
создаются с помощью prop clickable
равно true
(поскольку по умолчанию в состоянии приложения applicationBusy установлено значение false) - во внешней области регистрируются обработчики событий мыши
- во внутренней области регистрируются обработчики событий мыши
- пользователь нажимает кнопку мыши ввнутренняя область
- срабатывает прослушиватель mouseDown внутренней области
- срабатывает onClicking внутренней области
- контейнер выполняет действие setInteractionBusy
- избыточное состояние приложения
interactionBusy
isустановлен в true
- срабатывает прослушиватель mouseDown внешней области (это
clickable
prop по-прежнему true
, потому что, хотя состояние приложения interactionBusy
равно true
, реакция еще не вызвала повторную визуализацию;операция рендеринга помещается в конец цикла событий Javascript) - пользователь отпускает мышь вверх
- триггеры прослушивания mouseUp внутренней области
- консольные журналы "щелчок внутренней области"
- триггеры прослушивания mouseUp внешней области
- журналы консоли"external area click"
- реагирует повторно отображает
Component.jsx
и передает clickable
как false
обоим компонентам, но сейчас это слишком поздно
Это будетoutput:
inner area click
outer area click
Принимая во внимание, что желаемый вывод:
inner area click
Состояние приложения, следовательно, не помогает сделать эти компоненты более независимыми и многократно используемыми.Причина, по-видимому, заключается в том, что, хотя состояние обновляется, контейнер выполняет повторную визуализацию только в конце цикла событий, а триггеры распространения событий мыши уже поставлены в очередь в цикле событий.
Поэтому мой вопрос : есть ли альтернатива использованию состояния приложения, чтобы иметь возможность работать с распространением событий мыши декларативным образом, или есть лучший способ реализовать / выполнить мою вышеописанную настройку с помощью приложения (redux)состояние