Handle events outside of your React components with Flux
React components are pieces of knowledge that should deal with themselves and nothing else, to respect the separation of concerns principle that comes with the object-oriented programming paradigm. When using such an architecture, one might wonder how to handle JavaScript events happening outside of a component's scope, but still affecting it; a good example could be a dropdown menu that needs to be closed whenever the user clicks anywhere on the page, outside of it. Flux is actually able to answer such a problematic: let's figure out how!
Note: I will use a basic Flux implementation as described by Facebook.
First, let's define our
Dropdown
component:'use strict';
var React = require('react'),
classNames = require('classnames');
module.exports = React.createClass({
getInitialState: function() {
return {
open: false
};
},
toggle: function() {
this.setState({
open: !this.state.open
});
},
render: function() {
return (
<div className={classNames({ open: this.state.open })} onClick={this.toggle}>
{this.props.children}
</div>
);
}
});
We need to catch clicks happening throughout our app (which we assume lives in a
Root
component) and let Dropdown
know when they happen, so it can call its own toggle
to close itself up. In order to do so, we will simply follow the Flux pattern, and start by defining a constant for the click event in a browserConstants
module:'use strict';
module.exports = {
ACTION_CLICK: 'ACTION_CLICK'
};
We will also set up the related action, in a
browserActions
module:'use strict';
var browserConstants = require('../constants/browserConstants'),
dispatcher = require('../dispatcher');
module.exports = {
appClick: function() {
dispatcher.handleAction({
actionType: browserConstants.ACTION_CLICK
});
}
};
It is now time to implement our
browserStore
:'use strict';
var EventEmitter2 = require('eventemitter2').EventEmitter2,
dispatcher = require('../dispatcher'),
browserConstants = require('../constants/browserConstants'),
store = {};
Object.assign(
store,
EventEmitter2.prototype,
{
emitClick: function() {
this.emit('click');
},
addClickListener: function(callback) {
this.addListener('click', callback);
},
removeClickListener: function(callback) {
this.removeListener('click', callback);
}
}
);
dispatcher.register(function(payload) {
switch (payload.action.actionType) {
case browserConstants.ACTION_CLICK:
store.emitClick();
break;
}
return true;
});
module.exports = store;
We are now able to dispatch the click event by calling an action, which is the next thing we will do in our
Root
component:'use strict';
var React = require('react'),
browserActions = require('../actions/browserActions');
module.exports = React.createClass({
render: function() {
return (
<div onClick={browserActions.appClick}>
{/* your content here */}
</div>
);
}
});
The last thing to do is to listen and react to this event in
Dropdown
:'use strict';
var React = require('react'),
browserStore = require('../stores/browserStore'),
classNames = require('classnames');
module.exports = React.createClass({
getInitialState: function() {
return {
open: false
};
},
toggle: function() {
this.setState({
open: !this.state.open
});
},
onAppClick: function() {
if (this.state.open) {
this.toggle();
}
},
componentDidMount: function() {
browserStore.addClickListener(this.onAppClick);
},
componentWillUnmount: function() {
browserStore.removeClickListener(this.onAppClick);
},
render: function() {
return (
<div className={classNames({ open: this.state.open })} onClick={this.toggle}>
{this.props.children}
</div>
);
}
});
Now, clicking anywhere will close any open instance of this component! Messing around with the Flux pattern in such a way might prove itself useful in other contexts, feel free to leave a comment if you have anything to share on the subject.