Handle events outside of your React components with Flux

Flux is an architecture methodology for React applications proposed by Facebook to support its own UI library. It is designed to help inject data from the server (or any other data source) into the React components and the other way around.

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.