Disclaimer: This work and the technologies are not related to any Alfresco products.
There isn’t a great deal of documentation or “best practices” that best describes how ReactJS could or can even receive updates via web-sockets.
I trawled the internet for about 10 minutes, which is my typical snapping point of yelling “Fine, I’ll do it myself!”. The first step was to read up on the Observer pattern and I came across a picture and description that fits the bill. You can look at the pattern and description here:
https://en.wikipedia.org/wiki/Observer_pattern
I love UML diagrams. They are, at least to me, very clear and unambiguous. So this one leaped out at me. I decided to follow this diagram but removed the need for the ConcreteObservers as I had a different idea on how to simplify this even further.
All of the implementation, naturally, is in JS and my JS skills have room for improvement (whose doesn’t?). I am happy to receive feedback based on what you see.
So I firstly implemented the Subject module as such:
'use strict';
var _ = require('lodash');
module.exports = {
observerCollection: [],
registerObserver: function(observer) {
// to avoid over subscription, check the element isnt already in the array
var index = this.observerCollection.indexOf(observer);
if (index == -1) {
this.observerCollection.push(observer);
}
},
unregisterObserver: function(observer) {
_.remove(this.observerCollection, function(e) {
return observer === e;
});
},
notifyObservers: function(data) {
for (var observer in this.observerCollection) {
if (this.observerCollection.hasOwnProperty(observer)) {
var subscriber = this.observerCollection[observer];
if (typeof subscriber.notify === 'function') {
subscriber.notify(data);
} else {
console.warn('An observer was found without a notify function');
}
}
}
}
};
Pretty simple really. Three methods; registerObserver(), unregisterObserver() and notifyObservers(). The last method is the one methods signature I did have to change so that it accepted an argument. As you can see notifyObservers loops through each observer in a list and calls the notify() method passing in the data the observer may or may not be interested in.
Next I created the Web-sockets module, which calls the notifyObservers() method as seen below:
'use strict';
var Subject = require('./Subject');
module.exports = {
connection: new WebSocket('ws://localhost:8080/events'),
connect: function() {
var connection = this.connection;
// Setup events once connected
connection.onopen = function() {
connection.send('Ping');
};
connection.onerror = function(error) {
console.log(error);
};
// This function reacts anytime a message is received
connection.onmessage = function(e) {
console.log('From server: ' + e.data);
Subject.notifyObservers(e.data);
};
}
};
So this module utilises the HTML5 web-sockets library and this is as simple as it gets. Once a connection is established events that the connection should respond to need configuring. There are 3 events to be concerned with; onopen, onerror and onmessage.
Onopen is fired only once and handy for sending a message to the server. This message can then be checked in the logs to ensure a connection has been established.
Currently the onerror method only logs an error if the connection is lost or the connection could not be established.
The onmessage method is a callback method that fires every time a message is received from the server the web-socket is connected to. So from there it made the most sense, obviously, to call the notifyObservers() method passing in the data. The ‘e’ object is the full event object, but I was only concerned with the data received from the server. I followed the tutorial for HTML5 web-sockets at this link:
http://www.html5rocks.com/en/tutorials/websockets/basics/
Finally I had to get a ReactJS component to become the observer. Rather than having a “middle-man” observer class I decided to make the ReactJS component actually care about the messages it will receive and respond accordingly.
ReactJS components have a lifecycle. Methods are called chronologically as the component is either mounted, rendered or destroyed. If you want to read more on this lifecycle follow the link below:
https://facebook.github.io/react/docs/component-specs.html
A ReactJS component typically manages its own state. When the state is updated the component is then re-rendered. So the first thing the component needs to do is to register itself with the registerObserver() method as shown below:
componentDidMount: function() {
if (this.isMounted()) {
Subject.registerObserver(this);
}
},
Look at the line with the registerObserver() method. I am passing in the entire component with this. So now the component is added to the list of observers. Next, for the sake of cleanliness, I added the following method:
componentWillUnmount: function() {
Subject.unregisterObserver(this);
},
When the component is unmounted from the DOM, the component is removed from the list. The final part was to make sure that the component implemented a notify method as such:
notify: function(data) {
console.log('Receiving ' + data);
},
As part of the notifyObservers() loop, this method in the component will be called. Within each notify method in every component that is registered as an observer will be filtering of the JSON. Every component wont care about every message received. So for example we could filter on type as such:
{ type: “type I care about”, action: “action to take” }
This is how I implemented web-sockets and messaging with ReactJS. We have adopted ReactJS for the development of our new internal DevOps tools.
Next steps will involve further development on what I have shown you. I may update this blog with how we did the filtering of messages and any other improvements but for now, this works well for us.
Thanks all!