Global events and communication between React components

Motivation: I was working on a React component called AuthenticateMenu. Basically, it gives you a Login or Logout option depending on your OAuth2 authentication status with the backend. The problem I faced was that I didn’t want to make AuthenticateMenu a child of another component. It seemed perfectly reasonable that I should want this kind of organizational structure (simplified):

1
2
3
4
5
6
7
8
9
10
11
12
...
<body>
<div class="navbar" role="navigation">
...
<div id="authenticateMenu"></div> <!-- menu rendered here -->
...
</div>
...
<div id="mainApp"></div> <!-- main application rendered here -->
...
</body>
...

Reasonable. But, how does the rendered authenticateMenu tell the mainApp that the user has been authenticated? How do these, and other similarly arranged React components components talk amongst each other?

The official documentation says this:

For communication between two components that don’t have a parent-child relationship, you can set up your own global event system. Subscribe to events in componentDidMount(), unsubscribe in componentWillUnmount(), and when you receive an event, call setState().

I dare say, that is precisely what I did…

All this effort culminated in three components:

  1. AuthenticateMenu
  2. Authenticate
  3. HelloWorld

Simply put, HelloWorld shouts out to the whole world until a user authenticates through AuthenticateMenu. At this point, Authenticate, the global event listener, tells HelloWorld to greet the user instead.

Without delving into the intricacies of OAuth2, AuthenticateMenu calls on the server to authenticate through a popup menu. On success, the server returns an authentication token, which then needs to be verified. Once verified, AuthenticateMenu needs to notify HelloWorld.

Here are the relevant parts of AuthenticateMenu:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
var AuthenticateMenu = React.createClass({
//
// ...
//
/**
* Emit a 'verified' event each time verifyToken
* returns, whether on success or error
*/
dispatchEvent: function() {
var verifiedEvent = new CustomEvent('verified', { detail: this.state });
window.dispatchEvent(verifiedEvent);
},
/**
* Remember, the token was received through a popup window. When the user
* receives a token, the popup window emits a 'message' event, which
* carries the token to the AuthenticateMenu component.
*/
componentDidMount: function() {
// Listen for when the authentication dialog returns
// a token
var parent = this;
window.addEventListener('message', function(event) {
if (event.data.access_token) {
verifyToken(event.data.access_token, function(err, data) {
if (err) {
localStorage.clear(name + '-key');
parent.setState(parent.getInitialState(), parent.dispatchEvent);
}
else {
localStorage.setItem(name + '-key', event.data.access_token);
parent.setState({ accessToken: event.data.access_token,
name: data.name,
admin: data.admin,
verified: true }, parent.dispatchEvent);
}
});
}
else {
localStorage.clear(name + '-key');
parent.setState(parent.getInitialState(), parent.dispatchEvent);
}
});
},
//
// ...
//
});

If nothing else, note the parent.dispatchEvent callbacks. The HelloWorld component needs to know when the server approves or denies user access. The parent.dispatchEvent function accomplishes this by emitting a verified event, which is then picked up by the Authenticate component. Authenticate then executes the callback provided by the HelloWorld component.

As you can see, Authenticate is very simple:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var Authenticate = React.createClass({
/**
* componentDidMount
*/
componentDidMount: function() {
var update = this.props.update;
window.addEventListener('verified', function(event) {
update(event.detail);
});
},
/**
* Render the component
*/
render: function() {
return(
<div className='authenticate'></div>
);
}
});

As per the official documentation’s recommendation, I attached the listener in the componentDidMount() function. When a verified event is emitted by the AuthenticateMenu, the Authenticate component calls its parent’s callback (stored in this.props.update).

This arrangment allowed me to stick an Authenticate component inside of HelloWorld:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
var HelloWorld = React.createClass({
//
// ...
//
/**
* Called whenever the user's authentication status changes
*
* @param object
*/
authenticate: function(user) {
if (user.verified) {
this.setState({
friendo: user.name,
});
}
else {
this.setState(this.getInitialState());
}
},
/**
* Render the component
*/
render: function() {
return(
<div className='helloWorld' ref='helloWorld'>
<Authenticate ref='authenticated' update={this.authenticate} />
<h1>Hello, {this.state.friendo}!</h1>
</div>
);
}
});

This also allows me to stick an Authenticate component anywhere application functionality needs to be restricted to authorized users. Given this particular authentication-focussed example, the Authenticate intermediary makes the whole mishmash a lot more manageable.