React and Reflux usage in real-time applications based on websockets (Part 3— Client side application)
Before reading this story it’s strongly recommended to take a look at Part 2 to understand how does the server side work. The introduction and general description are presented in Part 1.
Client application folder structure is presented in the repository in ‘client’ folder
For serving assets and running development server we use webpack with React and ES6 support. You can find an example how to set it up here
The main component of our application is List which renders the users list itself and initializes the store
class List extends Reflux.Component {
constructor(props) {
super(props);
}
componentDidMount() {
let ids = [];
this.props.items.forEach((item) => {
ids.push(item.id);
});
Reflux.initStore(CounterStore);
CounterActions.init(ids);
}
render() {
return (
<div className=”list”>
{this.props.items.map((item, index) => {
item.key = index;
return <Item {…item} />
})}
</div>
);
}
};
After this component mounts, it creates an instance of CounterStore as a singleton, and calls init action defined in CounterActions.
const CounterActions = Reflux.createActions([
‘init’, ‘destroy’, ‘enable’, ‘disable’]);
This action triggers ‘onInit’ listener in store which creates the WebSocket connection
onInit(ids) {
this.ids = ids;
if (!this.socket) {
this.socket = io.connect(this.url);
}
this.handleConnect()
}
The store contains 3 main handlers for interaction with WebSockets— handleConnect subscribes to WebSocket updates with defined ids and starts listening for updates. Next method — handleUpdate — stores update packages in store’s state. Method handleDisconnect reacts on WebSocket disconnect from the server side.
handleConnect() {
if (!this.socket) {
return false;
}
this.socket.on(‘connect’, () => {
this.connected = true;
let ids = this.ids;
this.socket.emit(‘subscribe’, {ids}, () => {
this.handleUpdate();
});
this.handleDisconnect();
});
}handleUpdate() {
if (!this.socket) {
return false;
}
this.socket.on(‘update’, (data) => {
this.setState({[data.id]: data});
});
this.socket.on(‘status’, (data) => {
this.setState({[data.id]: data});
});
this.socket.on(‘error’, (data) => {
alert(data.message);
this.onDestroy();
});
}handleDisconnect() {
this.socket.on(‘disconnect’, () => {
this.connected = false;
this.setState({});
return;
});
}
Also the store contains two actions for sending messages to the server via websockets to enable/disable specific item
onEnable(id) {
this.socket.emit(‘enable’, {id});
}
onDisable(id) {
this.socket.emit(‘disable’, {id});
}
And another action to destroy WebSocket connection
onDestroy() {
this.ids = [];
this.socket.disconnect();
}
Counter component is rendered in each Item of the List. This component listens to store updates with Reflux mapStoreToState function. Basically, this function takes every change in the state of the store as an argument. The code inside this function provides kind of filter for update messages. It takes only package associated with this counter id and processes data to update component’s state according to WebSocket message transferred from the store’s state.
class Counter extends Reflux.Component { constructor(props) {
super(props);
this.state = {
number: 0,
status: ‘enabled’,
};
this.processData = this.processData.bind(this);
this.handleClick = this.handleClick.bind(this);
this.mapStoreToState(CounterStore, (data) => {
if (typeof data[this.props.id] !== ‘undefined’) {
return this.processData(data[this.props.id]);
}
});
} processData(data) {
let stateObj = Object.assign({}, this.state);
if (typeof data.error !== ‘undefined’) {
alert(data.error);
}
if (typeof data.number !== ‘undefined’
&& this.state.status !== ‘disabled’) {
stateObj.number = data.number;
}
if (typeof data.status !== ‘undefined’) {
stateObj.status = data.status;
}
return stateObj;
} handleClick(e) { //change status on click
if (this.state.status === ‘enabled’) {
CounterActions.disable(this.props.id);
}
if (this.state.status === ‘disabled’) {
CounterActions.enable(this.props.id);
}
} render() {
const counterClass = ‘list__counter’ + ‘ ‘
+ ‘list__counter — ‘ + this.state.status;
return (
<div onClick={this.handleClick} className={counterClass}>
{this.state.number}
</div>
);
}};
On ‘click’ event handleClick method triggers enable or disable action which invokes related listener in the store.
To test this application you should pull the repository, launch server application described in Part 2 of this story then go to the client folder, do npm install if needed and then you can run dev server using npm start command in your command line. The app should be accessible as http://localhost:8080/