After the last article the development workflow is setup ready for us to start iterating on the code needed to build the application features. In this article we are going to connect two of the chat clients we have built so far using a web socket connection with Socket.io.

This post is part of the Developing for a modern web with React.js series. If you’d like to be notified of new posts in this series you can become a free member. If you don’t want to complete the previous articles before working through this tutorial you can download the code from Github.

Start by installing the Socket.io package with NPM.

Install database packages npm install socket.io --save 1 npm install socket . io -- save

The socket.io package allows us to create a socket server that will handle connections from clients and direct chat messages between them. A socket server allows for real time bidirectional messaging between a client and server through a dedicated connection so that clients do not need to poll the server for changes. You can read more about web sockets on the MDN developer network. Create a new folder under the server directory named socket-server and create the new file ./server/socket-server/index.js with the following code.

Socket server import io from 'socket.io'; export default function (server) { const socketServer = io(server); const connections = []; socketServer.on('connection', socket => { connections.push(socket); socket.on('message', data => { connections.forEach(connectedSocket => { if (connectedSocket !== socket) { connectedSocket.emit('message', data); } }); }); socket.on('disconnect', () => { const index = connections.indexOf(socket); connections.splice(index, 1); }); }); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import io from 'socket.io' ; export default function ( server ) { const socketServer = io ( server ) ; const connections = [ ] ; socketServer . on ( 'connection' , socket = > { connections . push ( socket ) ; socket . on ( 'message' , data = > { connections . forEach ( connectedSocket = > { if ( connectedSocket ! == socket ) { connectedSocket . emit ( 'message' , data ) ; } } ) ; } ) ; socket . on ( 'disconnect' , ( ) = > { const index = connections . indexOf ( socket ) ; connections . splice ( index , 1 ) ; } ) ; } ) ; }

This socket-server module exports a single function that takes the Express application server as its only parameter. This simple implementation will serve us for the purpose of this article so that we can test the message passing between two browser tabs running the chat application. The socket server receives a message from one client and emits that same message to all other connected clients (we only want two to test out our chat for this article but it would work with more). Each socket connection is stored in an array called connections and when a message is received on a socket the list of connections is iterated over emitting the message to each one except for the socket that sent the message. When the socket disconnects it is removed from the connections array by splicing.

To initialise the socket server edit ./index.js to import the socket-server module and call the exported function passing it the Express application server.

Create the socket server import server from './server'; import http from 'http'; import socketServer from './server/socket-server'; var config = {}; if (process.env.NODE_ENV === 'development') { config.port = 3000; config.host = 'localhost'; server.locals.assetPath = 'http://localhost:8080/'; server.locals.isDevelopment = true; } const webServer = server.listen(config.port, config.host, err => { if (err) throw err; console.log('Web server listening at http://%s:%d', config.host, config.port); }); socketServer(webServer); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import server from './server' ; import http from 'http' ; import socketServer from './server/socket-server' ; var config = { } ; if ( process . env . NODE_ENV === 'development' ) { config . port = 3000 ; config . host = 'localhost' ; server . locals . assetPath = 'http://localhost:8080/' ; server . locals . isDevelopment = true ; } const webServer = server . listen ( config . port , config . host , err = > { if ( err ) throw err ; console . log ( 'Web server listening at http://%s:%d' , config . host , config . port ) ; } ) ; socketServer ( webServer ) ;

Now when the Express application server is started the socket server is running and waiting for clients to connect. To create a connection add the following code to the client application in the new file ./client/chat.js.

Client connection to socket server import io from 'socket.io-client'; const socket = io.connect(`${location.protocol}//${location.host}`); 1 2 3 import io from 'socket.io-client' ; const socket = io . connect ( ` $ { location . protocol } //${location.host}`);

In this module we include the client library that is bundled with Socket.io and create a connection to the server on the same host as our express application. To enable the communication between clients we need to be able to emit each added message to the server and listen for messages from other clients. First we’ll add the code to listen for a message and dispatch a new action to add the received message to the chat, let’s refer to these messages as responses. Edit ./client/chat.js and wrap the socket connection in an exported function that takes the store as it’s only argument.

Wrap the connection in an exported function import * as actions from 'actions/message-actions'; import io from 'socket.io-client'; export default function (store) { const socket = io.connect(`${location.protocol}//${location.host}`); socket.on('message', message => { store.dispatch(actions.addResponse(message)); }); } 1 2 3 4 5 6 7 8 9 10 import * as actions from 'actions/message-actions' ; import io from 'socket.io-client' ; export default function ( store ) { const socket = io . connect ( ` $ { location . protocol } //${location.host}`); socket . on ( 'message' , message = > { store . dispatch ( actions . addResponse ( message ) ) ; } ) ; }

The message action creators are imported and a the new action creator addResponse is used to create the action that is dispatched to the store when a message is recevied on the socket. Edit ./client/index.js to import the chat module and start the chat once the store is created.

Start the chat import React from 'react'; import ReactDOM from 'react-dom'; import {createStore} from 'redux'; import {Provider} from 'react-redux'; import App from 'components/app'; import reducers from 'reducers'; import startChat from './chat'; const initialState = window.INITIAL_STATE; const store = createStore(reducers(initialState)); startChat(store); ReactDOM.render( <Provider store={store}> <App /> </Provider> , document.getElementById('app')); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import React from 'react' ; import ReactDOM from 'react-dom' ; import { createStore } from 'redux' ; import { Provider } from 'react-redux' ; import App from 'components/app' ; import reducers from 'reducers' ; import startChat from './chat' ; const initialState = window . INITIAL_STATE ; const store = createStore ( reducers ( initialState ) ) ; startChat ( store ) ; ReactDOM . render ( < Provider store = { store } > < App / > < / Provider > , document . getElementById ( 'app' ) ) ;

Add the new addResponse action creator in ./client/actions/message-actions.js.

Add the addResponse action creator export const UPDATE_MESSAGE = 'update-message'; export const ADD_MESSAGE = 'add-message'; export const ADD_RESPONSE = 'add-response'; export function updateMessage(message) { return { type: UPDATE_MESSAGE, message }; } export function addMessage() { return { type: ADD_MESSAGE }; } export function addResponse(message) { return { type: ADD_RESPONSE, message }; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 export const UPDATE_MESSAGE = 'update-message' ; export const ADD_MESSAGE = 'add-message' ; export const ADD_RESPONSE = 'add-response' ; export function updateMessage ( message ) { return { type : UPDATE_MESSAGE , message } ; } export function addMessage ( ) { return { type : ADD_MESSAGE } ; } export function addResponse ( message ) { return { type : ADD_RESPONSE , message } ; }

The new action needs to be handled in the reducer function so that the new message is added to the list of messages. Adding another case to the switch statement in the reducer function starts to raise a bit of a code smell. Look at how this might look.

Handle the ADD_RESPONSE action type import {UPDATE_MESSAGE, ADD_MESSAGE, ADD_RESPONSE} from 'actions/message-actions' export default function (initialState) { return (state=initialState, action) => { var messages; switch(action.type) { case UPDATE_MESSAGE: return Object.assign({}, state, { currentMessage: action.message }); case ADD_MESSAGE: const text = state.currentMessage.trim(); if (text) { messages = state.messages.map(message => Object.assign({}, message)); messages.push({id: messages.length + 1, text}); return { messages, currentMessage: '' }; } case ADD_RESPONSE: messages = state.messages.map(message => Object.assign({}, message)); messages.push(Object.assign({isAdmin: true}, action.message)); return Object.assign({}, state, {messages}); default: return state; } } } 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 import { UPDATE_MESSAGE , ADD_MESSAGE , ADD_RESPONSE } from 'actions/message-actions' export default function ( initialState ) { return ( state = initialState , action ) = > { var messages ; switch ( action . type ) { case UPDATE_MESSAGE : return Object . assign ( { } , state , { currentMessage : action . message } ) ; case ADD_MESSAGE : const text = state . currentMessage . trim ( ) ; if ( text ) { messages = state . messages . map ( message = > Object . assign ( { } , message ) ) ; messages . push ( { id : messages . length + 1 , text } ) ; return { messages , currentMessage : '' } ; } case ADD_RESPONSE : messages = state . messages . map ( message = > Object . assign ( { } , message ) ) ; messages . push ( Object . assign ( { isAdmin : true } , action . message ) ) ; return Object . assign ( { } , state , { messages } ) ; default : return state ; } } }

There is code duplication and we can see that the addition of any more actions will face similar problems. We could move shared code to other helper functions but it’s at this stage that we need to make a better choice and refactor the code. Redux provides a utility called combineReducers that can be used to compose the separate parts of the state in the store using a combination of reducer functions. To achieve the level of separation wanted we need to move the logic that trims the message and checks that it is not empty to the MessageEntryBox component. First edit ./client/reducers/index.js to incorporate combineReducers.

Refactor the reducers to separate state import {combineReducers} from 'redux'; import {UPDATE_MESSAGE, ADD_MESSAGE, ADD_RESPONSE} from 'actions/message-actions' export default function (initialState) { function messages(currentMessages=initialState.messages, action) { const messages = currentMessages.map(message => Object.assign({}, message)); switch(action.type) { case ADD_RESPONSE: messages.push(Object.assign({}, action.message)); break; case ADD_MESSAGE: messages.push({id: messages.length + 1, text: action.message}); } return messages; } function currentMessage(currentMessage=initialState.currentMessage, action) { switch(action.type) { case UPDATE_MESSAGE: return action.message; case ADD_MESSAGE: return ''; default: return currentMessage; } } return combineReducers({currentMessage, messages}); } 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 import { combineReducers } from 'redux' ; import { UPDATE_MESSAGE , ADD_MESSAGE , ADD_RESPONSE } from 'actions/message-actions' export default function ( initialState ) { function messages ( currentMessages = initialState . messages , action ) { const messages = currentMessages . map ( message = > Object . assign ( { } , message ) ) ; switch ( action . type ) { case ADD_RESPONSE : messages . push ( Object . assign ( { } , action . message ) ) ; break ; case ADD_MESSAGE : messages . push ( { id : messages . length + 1 , text : action . message } ) ; } return messages ; } function currentMessage ( currentMessage = initialState . currentMessage , action ) { switch ( action . type ) { case UPDATE_MESSAGE : return action . message ; case ADD_MESSAGE : return '' ; default : return currentMessage ; } } return combineReducers ( { currentMessage , messages } ) ; }

There are now two separate functions for managing the individual sections of the state, currentMessage and messages, and it’s now much easier to understand how these parts of the state change with each action. Edit the MessageEntryBox component in ./client/components/message-entry-box/index.js to send the trimmed message on submit.

Update message entry box to send message on submit import React, {Component} from 'react'; class MessageEntryBox extends Component { render() { return ( <div className='message-entry-box'> <textarea name='message' placeholder='Enter a message' value={this.props.value} onChange={this.handleChange.bind(this)} onKeyPress={this.handleKeyPress.bind(this)}/> </div> ); } handleChange(ev) { this.props.onChange(ev.target.value); } handleKeyPress(ev) { if (ev.which === 13) { const trimmedMessage = this.props.value.trim(); if (trimmedMessage) { this.props.onSubmit(trimmedMessage); } ev.preventDefault(); } } } export default MessageEntryBox; 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 import React , { Component } from 'react' ; class MessageEntryBox extends Component { render ( ) { return ( < div className = 'message-entry-box' > < textarea name = 'message' placeholder = 'Enter a message' value = { this . props . value } onChange = { this . handleChange . bind ( this ) } onKeyPress = { this . handleKeyPress . bind ( this ) } / > < / div > ) ; } handleChange ( ev ) { this . props . onChange ( ev . target . value ) ; } handleKeyPress ( ev ) { if ( ev . which === 13 ) { const trimmedMessage = this . props . value . trim ( ) ; if ( trimmedMessage ) { this . props . onSubmit ( trimmedMessage ) ; } ev . preventDefault ( ) ; } } } export default MessageEntryBox ;

Lastly update the action creator to include the message.

Add message to the action creator export const UPDATE_MESSAGE = 'update-message'; export const ADD_MESSAGE = 'add-message'; export const ADD_RESPONSE = 'add-response'; export function updateMessage(message) { return { type: UPDATE_MESSAGE, message }; } export function addMessage(message) { return { type: ADD_MESSAGE, message }; } export function addResponse(message) { return { type: ADD_RESPONSE, message }; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 export const UPDATE_MESSAGE = 'update-message' ; export const ADD_MESSAGE = 'add-message' ; export const ADD_RESPONSE = 'add-response' ; export function updateMessage ( message ) { return { type : UPDATE_MESSAGE , message } ; } export function addMessage ( message ) { return { type : ADD_MESSAGE , message } ; } export function addResponse ( message ) { return { type : ADD_RESPONSE , message } ; }

Adding the Redux chat middleware

To send messages to the server we are going to create a middleware for Redux. Edit ./client/chat.js and make the following changes to export a middleware function called chatMiddleware.

Add the chat middleware import * as actions from 'actions/message-actions'; import io from 'socket.io-client'; var socket = null; export function chatMiddleware(store) { return next => action => { const result = next(action); if (socket && action.type === actions.ADD_MESSAGE) { let messages = store.getState().messages; socket.emit('message', messages[messages.length -1]); } return result; }; } export default function (store) { socket = io.connect(`${location.protocol}//${location.host}`); socket.on('message', data => { store.dispatch(actions.addResponse(data)); }); } 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 import * as actions from 'actions/message-actions' ; import io from 'socket.io-client' ; var socket = null ; export function chatMiddleware ( store ) { return next = > action = > { const result = next ( action ) ; if ( socket && action.type === actions.ADD_MESSAGE) { let messages = store.getState().messages; socket . emit ( 'message' , messages [ messages . length - 1 ] ) ; } return result ; } ; } export default function ( store ) { socket = io . connect ( ` $ { location . protocol } //${location.host}`); socket . on ( 'message' , data = > { store . dispatch ( actions . addResponse ( data ) ) ; } ) ; }

This middleware function returns a function which applies the next action and if the action is of the ADD_MESSAGE type plucks the last message off the list and sends it to the server. Edit ./client/index.js to Import the middleware and apply it to the store using the applyMiddleware util from Redux.

Add the chat middleware import React from 'react'; import ReactDOM from 'react-dom'; import {createStore, applyMiddleware} from 'redux'; import {Provider} from 'react-redux'; import App from 'components/app'; import reducers from 'reducers'; import startChat, {chatMiddleware} from './chat'; const initialState = window.INITIAL_STATE; const createStoreWithMiddleware = applyMiddleware(chatMiddleware)(createStore); const store = createStoreWithMiddleware(reducers(initialState)); startChat(store); ReactDOM.render( <Provider store={store}> <App /> </Provider> , document.getElementById('app')); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import React from 'react' ; import ReactDOM from 'react-dom' ; import { createStore , applyMiddleware } from 'redux' ; import { Provider } from 'react-redux' ; import App from 'components/app' ; import reducers from 'reducers' ; import startChat , { chatMiddleware } from './chat' ; const initialState = window . INITIAL_STATE ; const createStoreWithMiddleware = applyMiddleware ( chatMiddleware ) ( createStore ) ; const store = createStoreWithMiddleware ( reducers ( initialState ) ) ; startChat ( store ) ; ReactDOM . render ( < Provider store = { store } > < App / > < / Provider > , document . getElementById ( 'app' ) ) ;

Now every action will pass through this middleware function when dispatched and the two way communication with messages is established. Build and run the application using the npm scripts added in the last article. Once running open the chat application in two separate browser tabs and start a conversation with yourself. You should see the messages coming into the chat from the client in the other browser tab as you add them.

It’s working but the chat window is looking like it needs some styling and we can’t yet distinguish which message came from who. Before styling the chat window components let’s get the shape of the data for a message sorted.

We need to be able to identify each user so that we can assign the users identifier to each message. This way we can check the user id on each message to determine which messages are responses. Modify the socket server code to emit a new start event on successful connection that assigns an id to the client. Until we have a database installed we will use a simple counter for the user id. Edit ./server/socket-server/index.js and add the start event with the userId counter.

Add a user id for each connection import io from 'socket.io'; export default function (server) { const socketServer = io(server); const connections = []; var userId = 0; socketServer.on('connection', socket => { connections.push(socket); userId += 1; socket.emit('start', {userId}); socket.on('message', data => { connections.forEach(connectedSocket => { if (connectedSocket !== socket) { connectedSocket.emit('message', data); } }); }); socket.on('disconnect', () => { const index = connections.indexOf(socket); connections.splice(index, 1); }); }); } 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 import io from 'socket.io' ; export default function ( server ) { const socketServer = io ( server ) ; const connections = [ ] ; var userId = 0 ; socketServer . on ( 'connection' , socket = > { connections . push ( socket ) ; userId += 1 ; socket . emit ( 'start' , { userId } ) ; socket . on ( 'message' , data = > { connections . forEach ( connectedSocket = > { if ( connectedSocket ! == socket ) { connectedSocket . emit ( 'message' , data ) ; } } ) ; } ) ; socket . on ( 'disconnect' , ( ) = > { const index = connections . indexOf ( socket ) ; connections . splice ( index , 1 ) ; } ) ; } ) ; }

On the client we need to handle the start event by creating a new action to set the user id in the store. Edit ./client/chat.js and add the code to handle the start event.

Add handler for start event import * as actions from 'actions/message-actions'; import io from 'socket.io-client'; const socket = null; export function chatMiddleware(store) { return next => action => { const result = next(action); if (action.type === actions.ADD_MESSAGE) { let messages = store.getState().messages; socket.emit('message', messages[messages.length -1]); } return result; }; } export default function (store) { socket = io.connect(`${location.protocol}//${location.host}`); socket.on('start', data => { store.dispatch(actions.setUserId(data.userId)); }); socket.on('message', data => { store.dispatch(actions.addResponse(data)); }); } 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 import * as actions from 'actions/message-actions' ; import io from 'socket.io-client' ; const socket = null ; export function chatMiddleware ( store ) { return next = > action = > { const result = next ( action ) ; if ( action . type === actions . ADD_MESSAGE ) { let messages = store . getState ( ) . messages ; socket . emit ( 'message' , messages [ messages . length - 1 ] ) ; } return result ; } ; } export default function ( store ) { socket = io . connect ( ` $ { location . protocol } //${location.host}`); socket . on ( 'start' , data = > { store . dispatch ( actions . setUserId ( data . userId ) ) ; } ) ; socket . on ( 'message' , data = > { store . dispatch ( actions . addResponse ( data ) ) ; } ) ; }

Define the setUserId action creator in ./client/actions/message-actions.js

Add the user id action creator export const UPDATE_MESSAGE = 'update-message'; export const ADD_MESSAGE = 'add-message'; export const ADD_RESPONSE = 'add-response'; export const SET_USER_ID = 'setUserId'; export function updateMessage(message) { return { type: UPDATE_MESSAGE, message }; } export function addMessage(message) { return { type: ADD_MESSAGE, message }; } export function addResponse(message) { return { type: ADD_RESPONSE, message }; } export function setUserId(userId) { return { type: SET_USER_ID, userId }; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 export const UPDATE_MESSAGE = 'update-message' ; export const ADD_MESSAGE = 'add-message' ; export const ADD_RESPONSE = 'add-response' ; export const SET_USER_ID = 'setUserId' ; export function updateMessage ( message ) { return { type : UPDATE_MESSAGE , message } ; } export function addMessage ( message ) { return { type : ADD_MESSAGE , message } ; } export function addResponse ( message ) { return { type : ADD_RESPONSE , message } ; } export function setUserId ( userId ) { return { type : SET_USER_ID , userId } ; }

And add the reducer function for managing the userId attribute in ./client/reducers/index.js.

Add the userId reducer function import {combineReducers} from 'redux'; import {UPDATE_MESSAGE, ADD_MESSAGE, ADD_RESPONSE, SET_USER_ID} from 'actions/message-actions'; export default function (initialState) { function messages(currentMessages=initialState.messages, action) { const messages = currentMessages.map(message => Object.assign({}, message)); switch(action.type) { case ADD_RESPONSE: messages.push(Object.assign({}, action.message)); break; case ADD_MESSAGE: messages.push({id: messages.length + 1, text: action.message}); } return messages; } function currentMessage(currentMessage=initialState.currentMessage, action) { switch(action.type) { case UPDATE_MESSAGE: return action.message; case ADD_MESSAGE: return ''; default: return currentMessage; } } function userId(currentUserId=initialState.userId, action) { if (action.type === SET_USER_ID) { return action.userId; } return currentUserId; } return combineReducers({userId, currentMessage, messages}); } 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 import { combineReducers } from 'redux' ; import { UPDATE_MESSAGE , ADD_MESSAGE , ADD_RESPONSE , SET_USER_ID } from 'actions/message-actions' ; export default function ( initialState ) { function messages ( currentMessages = initialState . messages , action ) { const messages = currentMessages . map ( message = > Object . assign ( { } , message ) ) ; switch ( action . type ) { case ADD_RESPONSE : messages . push ( Object . assign ( { } , action . message ) ) ; break ; case ADD_MESSAGE : messages . push ( { id : messages . length + 1 , text : action . message } ) ; } return messages ; } function currentMessage ( currentMessage = initialState . currentMessage , action ) { switch ( action . type ) { case UPDATE_MESSAGE : return action . message ; case ADD_MESSAGE : return '' ; default : return currentMessage ; } } function userId ( currentUserId = initialState . userId , action ) { if ( action . type === SET_USER_ID ) { return action . userId ; } return currentUserId ; } return combineReducers ( { userId , currentMessage , messages } ) ; }

With these changes we now have the userId saved in the store state so we need to add it to the initial state we have in our server code. Edit ./server/index.js and add an empty string for the initial userId attribute.

Add the user id to the initial state import path from 'path'; import express from 'express'; import handlebars from 'express-handlebars'; import React from 'react'; import ReactDOMServer from 'react-dom/server'; import {createStore} from 'redux'; import {Provider} from 'react-redux'; import App from './generated/app'; const app = express(); // View templates app.engine('handlebars', handlebars({ defaultLayout: 'main', layoutsDir: path.resolve(__dirname, 'views/layouts') })); app.set('view engine', 'handlebars'); app.set('views', path.resolve(__dirname, 'views')); // Static assets app.use(express.static(path.resolve(__dirname, '../dist'))); // Routes app.get('/', (request, response) => { const initialState = { userId: '', currentMessage: '', messages: [] }; const store = createStore((state=initialState) => state); const appContent = ReactDOMServer.renderToString( <Provider store={store}> <App /> </Provider> ); response.render('app', { app: appContent, initialState: JSON.stringify(initialState) }); }); export default app; 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 import path from 'path' ; import express from 'express' ; import handlebars from 'express-handlebars' ; import React from 'react' ; import ReactDOMServer from 'react-dom/server' ; import { createStore } from 'redux' ; import { Provider } from 'react-redux' ; import App from './generated/app' ; const app = express ( ) ; // View templates app . engine ( 'handlebars' , handlebars ( { defaultLayout : 'main' , layoutsDir : path . resolve ( __dirname , 'views/layouts' ) } ) ) ; app . set ( 'view engine' , 'handlebars' ) ; app . set ( 'views' , path . resolve ( __dirname , 'views' ) ) ; // Static assets app . use ( express . static ( path . resolve ( __dirname , '../dist' ) ) ) ; // Routes app . get ( '/' , ( request , response ) = > { const initialState = { userId : '' , currentMessage : '' , messages : [ ] } ; const store = createStore ( ( state = initialState ) = > state ) ; const appContent = ReactDOMServer . renderToString ( < Provider store = { store } > < App / > < / Provider > ) ; response . render ( 'app' , { app : appContent , initialState : JSON . stringify ( initialState ) } ) ; } ) ; export default app ;

The userId needs to be added to each message but we don’t want to muddle the concerns in our reducer functions. What we will do is replace the id we currently add to each message and compose the whole message from the MessageEntryBox component. Edit ./client/components/app/index.js and map the new userId property to the component props.

Map user id to component props import React, {Component} from 'react'; import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; import {} from './style.less'; import MessageList from 'components/message-list'; import MessageEntryBox from 'components/message-entry-box'; import * as messageActionCreators from 'actions/message-actions'; class App extends Component { render() { return ( <div> <MessageList userId={this.props.userId} messages={this.props.messages}/> <MessageEntryBox value={this.props.currentMessage} userId={this.props.userId} onChange={this.props.updateMessage} onSubmit={this.props.addMessage}/> </div> ); } } function mapStateToProps(state) { return { userId: state.userId, messages: state.messages, currentMessage: state.currentMessage }; } function mapDispatchToProps(dispatch) { return bindActionCreators({ addMessage: messageActionCreators.addMessage, updateMessage: messageActionCreators.updateMessage }, dispatch); } export default connect(mapStateToProps, mapDispatchToProps)(App); 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 import React , { Component } from 'react' ; import { connect } from 'react-redux' ; import { bindActionCreators } from 'redux' ; import { } from './style.less' ; import MessageList from 'components/message-list' ; import MessageEntryBox from 'components/message-entry-box' ; import * as messageActionCreators from 'actions/message-actions' ; class App extends Component { render ( ) { return ( < div > < MessageList userId = { this . props . userId } messages = { this . props . messages } / > < MessageEntryBox value = { this . props . currentMessage } userId = { this . props . userId } onChange = { this . props . updateMessage } onSubmit = { this . props . addMessage } / > < / div > ) ; } } function mapStateToProps ( state ) { return { userId : state . userId , messages : state . messages , currentMessage : state . currentMessage } ; } function mapDispatchToProps ( dispatch ) { return bindActionCreators ( { addMessage : messageActionCreators . addMessage , updateMessage : messageActionCreators . updateMessage } , dispatch ) ; } export default connect ( mapStateToProps , mapDispatchToProps ) ( App ) ;

The userId is now passed into both the MessageList and MessageEntryBox components. Update ./client/components/message-entry-box/index.js to compose the message object with the text and user id.

Update message to incorporporate userId import React, {Component} from 'react'; import {} from './style.less'; class MessageEntryBox extends Component { render() { return ( <div className='message-entry-box'> <textarea name='message' placeholder='Enter a message' value={this.props.value} onChange={this.handleChange.bind(this)} onKeyPress={this.handleKeyPress.bind(this)}/> </div> ); } handleChange(ev) { this.props.onChange(ev.target.value); } handleKeyPress(ev) { if (ev.which === 13) { const trimmedMessage = this.props.value.trim(); if (trimmedMessage) { this.props.onSubmit({ text: trimmedMessage, userId: this.props.userId }); } ev.preventDefault(); } } } export default MessageEntryBox; 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 import React , { Component } from 'react' ; import { } from './style.less' ; class MessageEntryBox extends Component { render ( ) { return ( < div className = 'message-entry-box' > < textarea name = 'message' placeholder = 'Enter a message' value = { this . props . value } onChange = { this . handleChange . bind ( this ) } onKeyPress = { this . handleKeyPress . bind ( this ) } / > < / div > ) ; } handleChange ( ev ) { this . props . onChange ( ev . target . value ) ; } handleKeyPress ( ev ) { if ( ev . which === 13 ) { const trimmedMessage = this . props . value . trim ( ) ; if ( trimmedMessage ) { this . props . onSubmit ( { text : trimmedMessage , userId : this . props . userId } ) ; } ev . preventDefault ( ) ; } } } export default MessageEntryBox ;

Now both the ADD_RESPONSE and ADD_MESSAGE actions can be handled in the same way. We do need to keep the distinction though as the messages are sent to the socket server when the ADD_MESSAGE action is dispatched. Edit./client/reducers/index.js and refactor the messages reducer function.

Refactored reducer functions import {combineReducers} from 'redux'; import { UPDATE_MESSAGE, ADD_MESSAGE, ADD_RESPONSE, SET_USER_ID } from 'actions/message-actions' export default function (initialState) { function messages(currentMessages=initialState.messages, action) { switch (action.type) { case ADD_MESSAGE: case ADD_RESPONSE: let messages = currentMessages.map(message => Object.assign({}, message)); messages.push(Object.assign({}, action.message)); return messages; default: return currentMessages; } } function currentMessage(currentMessage=initialState.currentMessage, action) { switch(action.type) { case UPDATE_MESSAGE: return action.message; case ADD_MESSAGE: return ''; default: return currentMessage; } } function userId(currentUserId=initialState.userId, action) { if (action.type === SET_USER_ID) { return action.userId; } return currentUserId; } return combineReducers({ currentMessage, messages, userId }); } 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 import { combineReducers } from 'redux' ; import { UPDATE_MESSAGE , ADD_MESSAGE , ADD_RESPONSE , SET_USER_ID } from 'actions/message-actions' export default function ( initialState ) { function messages ( currentMessages = initialState . messages , action ) { switch ( action . type ) { case ADD_MESSAGE : case ADD_RESPONSE : let messages = currentMessages . map ( message = > Object . assign ( { } , message ) ) ; messages . push ( Object . assign ( { } , action . message ) ) ; return messages ; default : return currentMessages ; } } function currentMessage ( currentMessage = initialState . currentMessage , action ) { switch ( action . type ) { case UPDATE_MESSAGE : return action . message ; case ADD_MESSAGE : return '' ; default : return currentMessage ; } } function userId ( currentUserId = initialState . userId , action ) { if ( action . type === SET_USER_ID ) { return action . userId ; } return currentUserId ; } return combineReducers ( { currentMessage , messages , userId } ) ; }

Composing the message object in the component will also allow us to simplify the chat middleware as the whole message is available in the action. Refactor the middleware in ./client/chat.js.

Refactored chat middleware import * as actions from 'actions/message-actions'; import io from 'socket.io-client'; var socket = null; export function chatMiddleware(store) { return next => action => { if (socket && action.type === actions.ADD_MESSAGE) { socket.emit('message', action.message); } return next(action); }; } export default function (store) { socket = io.connect(`${location.protocol}//${location.host}`); socket.on('start', data => { store.dispatch(actions.setUserId(data.userId)); }); socket.on('message', data => { store.dispatch(actions.addResponse(data)); }); } 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 import * as actions from 'actions/message-actions' ; import io from 'socket.io-client' ; var socket = null ; export function chatMiddleware ( store ) { return next = > action = > { if ( socket && action.type === actions.ADD_MESSAGE) { socket.emit('message', action.message); } return next ( action ) ; } ; } export default function ( store ) { socket = io . connect ( ` $ { location . protocol } //${location.host}`); socket . on ( 'start' , data = > { store . dispatch ( actions . setUserId ( data . userId ) ) ; } ) ; socket . on ( 'message' , data = > { store . dispatch ( actions . addResponse ( data ) ) ; } ) ; }

Last thing that needs to be done is to edit the MessageList component and add a class name to messages that are responses. Edit ./client/components/message-list/index.js and add the is-response class name to every message where the userId does not match the userId in the component props. The HTML structure is also updated so that the messages can be styled more easily.

Add userId check in message list component import React, {Component} from 'react'; class MessageList extends Component { render() { return ( <ol className='message-list'> {this.props.messages.map((message, index) => { const messageClass = message.userId !== this.props.userId ? 'is-response' : ''; return ( <li key={`message-${index}`} className='message-item'> <p className={`message ${messageClass}`}> {message.text} </p> </li> ); })} </ol> ); } } export default MessageList; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import React , { Component } from 'react' ; class MessageList extends Component { render ( ) { return ( < ol className = 'message-list' > { this . props . messages . map ( ( message , index ) = > { const messageClass = message . userId ! == this . props . userId ? 'is-response' : '' ; return ( < li key = { ` message - $ { index } ` } className = 'message-item' > < p className = { ` message $ { messageClass } ` } > { message . text } < / p > < / li > ) ; } ) } < / ol > ) ; } } export default MessageList ;

Styling the components

Okay, it’s about time we addressed the chat window styling. Feel free to style it how you would like or alternatively use the styles for the components shown below.

First edit ./client/components/app/style.less

Style app component html { background-color: #f2f2f2; font-family: Helvetica, Arial, sans-serif; } #app { background-color: white; box-shadow: 0 0 8px #dedede; margin: 20px auto; max-width: 400px; } 1 2 3 4 5 6 7 8 9 10 11 html { background - color : #f2f2f2; font - family : Helvetica , Arial , sans - serif ; } #app { background - color : white ; box - shadow : 0 0 8px #dedede; margin : 20px auto ; max - width : 400px ; }

then create the style sheet ./client/message-entry-box/style.less not forgetting to import it at the top of ./client/message-entry-box/index.js.

Import the styles import {} from './style.less'; 1 import { } from './style.less' ;

Style message entry box .message-entry-box { background-color: #dedede; padding: 10px; textarea { border: none; box-sizing: border-box; font-size: 14px; height: 60px; margin: 0; padding: 4px; width: 100%; &:focus { outline: none; } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 . message - entry - box { background - color : #dedede; padding : 10px ; textarea { border : none ; box - sizing : border - box ; font - size : 14px ; height : 60px ; margin : 0 ; padding : 4px ; width : 100 % ; &:focus { outline: none; } } }

Last of all, create ./client/components/message-list/style.less again not forgetting to import it in the component index.js file.

Style message list component .message-list { height: 240px; list-style: none; margin: 0; overflow: auto; padding: 10px; .message-item { overflow: hidden; margin: 0 0 6px; padding: 0; } .message { background-color: rgba(34, 114, 234, 0.4); border-radius: 8px; box-sizing: border-box; float: right; font-size: 14px; margin: 0; max-width: 90%; padding: 8px; &.is-response { background-color: rgba(12, 234, 88, 0.4); float: left; } } } 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 . message - list { height : 240px ; list - style : none ; margin : 0 ; overflow : auto ; padding : 10px ; . message - item { overflow : hidden ; margin : 0 0 6px ; padding : 0 ; } . message { background - color : rgba ( 34 , 114 , 234 , 0.4 ) ; border - radius : 8px ; box - sizing : border - box ; float : right ; font - size : 14px ; margin : 0 ; max - width : 90 % ; padding : 8px ; &.is-response { background-color: rgba(12, 234, 88, 0.4); float : left ; } } }

Restart the servers if not already running and make conversation again between two browser tabs. You should see response messages shown on the other side of the chat in a different colour to the main messages.

There is quite a lot to take in from this article especially the way the code is refactored as more functionality is added. Refactoring is a common practice during development and a skill that is developed over time. Ways to separate concerns and reduce duplication where identified and addressed making the code easier to read and ultimately more maintainable.

This post is part of the Developing for a modern web with React.js series. If you enjoyed the tutorial and would like to follow along to future posts please sign up to become a free member.