Simple Chat App - React with Socket.IO
19 May 2019Table of Contents
- Table of Contents
- Socket.IO 101
- Project Structure
- Installation
- Express Server
- React Client
- Connect with Socket
- Sending and Receiving Messages
- Broadcasting Messages
- Using Nicknames
One thing that I love Socket.IO is simple wheareas powerful. With handful of lines of codes, you can build a chat app. All you have to do is adding few lines to your existing web app.
If you are newby and wanna implement Socket.IO in your project, you’re on the right place. If you are not a newby but you wanna use it with React, than you’re also on the right place.
Today we will make a simple chat app with React and Socket.IO.
Socket.IO 101
Sockets were the solution for real-time communication. You can push messages to the client from the server.
One thing you need to know is, Socket.IO is not a WebSocket implementation.
Socket.IO is NOT a WebSocket implementation. Although Socket.IO indeed uses WebSocket as a transport when possible, it adds some metadata to each packet: the packet type, the namespace and the ack id when a message acknowledgement is needed. That is why a WebSocket client will not be able to successfully connect to a Socket.IO server, and a Socket.IO client will not be able to connect to a WebSocket server either.
ref: https://socket.io/docs/
Socket.IO offers followings:
- connectable even with proxies, load balancers, personal firewall and antivirus software.
- auto-reconnection support
- disconnection detection
- binary support
- room support
Project Structure
├── client
│ ├── README.md
│ ├── node_modules
│ ├── package.json
│ ├── public
│ │ ├── favicon.ico
│ │ ├── index.html
│ │ └── manifest.json
│ ├── src
│ │ ├── App.css
│ │ ├── App.js
│ │ ├── index.js
│ │ └── serviceWorker.js
│ └── yarn.lock
└── server
├── node_modules
├── index.js
├── package-lock.json
└── package.json
This is our project structure. React app will be located in the client directory. Node/Express server will be located in the server directory.
Since we are going to make our React app with CRA(create-react-app), it automatically offers a server for development environment.
So, our server directory is for API server, not static server for React app.
Installation
mkdir chat && cd chat
Press enter until you see success message.
Create server directory and change it to server. Then, initiate project with npm.
mkdir server && cd server
npm init
Then install node, express and socket
npm install --save node express socket.io
npm install --save-dev concurrently
“concurrently” will run your client/server at dev.
Then create-react-app with dir name client.
npm create-react-app client
cd client
npm install --save socket.io-client
“socket.io-client” is a client-side lib for Socket.IO.
Lastly, we need to add som script to run our two servers(node/express chat server and React client-side server).
server/package.json
{
...
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"client": "npm run start --prefix ../client",
"start": "node index.js",
"dev": "concurrently \"npm run start\" \"npm run client\""
},
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.17.1",
"node": "^12.3.1",
"socket.io": "^2.2.0"
},
"devDependencies": {
"concurrently": "^4.1.0"
}
}
Add scripts for running two servers concurrently.
At this point, you can test your React app with following command:
npm run dev
If you go to “http://localhost:3000”, you will see React icon spinning with dark grey background.
Express Server
Now is the time to set our chat server.
Inside the server directory, create index.js file.
server/index.js
const express = require("express");
const app = express();
const server = require("http").Server(app);
const io = require("socket.io")(server);
const PORT = process.env.PORT || 5000;
server.listen(PORT, () => console.log(`Listen on *: ${PORT}`));
Firstly, import express, http, socket.io to create chat server. Then set PORT.
const PORT = process.env.PORT || 5000;
This code sets port with 5000 if our server is running on localhost.
When node is running on real server, it offers “process.env.PORT”, so we can use it instead of 5000.
Done? Then we need to add some code to run Socket.IO.
io.on("connection", socket => {
const { id } = socket.client;
console.log(`User connected: ${id}`);
});
“socket.client” will provide id of the client. So the code above basically means whenever a user is connected, log userId on the console.
In order to see the outcome, we need to add some code on client-side. We will comeback to server to add more features like getting and broadcasting messages and using user nicknames.
React Client
Delete all other codes and add following:
client/src/App.js
import React from "react";
import io from "socket.io-client";
const socket = io.connect("http://localhost:5000");
function App() {
return <div />;
}
export default App;
Code below connects chat server running on “localhost:5000”.
const socket = io.connect("http://localhost:5000");
Connect with Socket
Now, we are ready to test our chat app. Change directory to server and run “npm run dev” command.
You will encounter message like this on your terminal.
User connected: wpjY_7eDVNoV5vD5AAAA
Actual userId may differ.
If you open another browser and go to “localhost:3000” where React client is running on, you will see another message in your terminal.
User connected: wpjY_7eDVNoV5vD5AAAA
User connected: T7jFW_AB04woSrhzAAAB
Whenever user comes to our React client, the server will console log userId.
Sending and Receiving Messages
It’s time to add message communication to our app. Change your directory to server.
In order to receive and broadcast messages, we need to add codes to both client and server.
server/index.js
socket.on("chat message", msg => {
console.log(`${id}: ${msg}`);
});
Add this code just below of console logging userId.
socket.on("chat message", msg => {});
This line is about receiving messages from the users. Any messages come from users will contained msg variable.
console.log(`${id}: ${msg}`);
This will log userId and message on your terminal.
At this point, our entire code on “server/index.js” looks like:
server/index.js
const server = require("http").Server(app);
const io = require("socket.io")(server);
io.on("connection", socket => {
const { id } = socket.client;
console.log(`User connected: ${id}`);
socket.on("chat message", msg => {
console.log(`${id}: ${msg}`);
});
});
const PORT = process.env.PORT || 5000;
server.listen(PORT, () => console.log(`Listen on *: ${PORT}`));
Change your directory to client
client/src/App.js
// Import Component
import React, { Component } from "react";
import io from "socket.io-client";
const socket = io.connect("http://localhost:5000");
// Change to class component
class App extends Component {
// Add constructor to initiate
constructor() {
super();
this.state = { msg: "" };
}
// Function for getting text input
onTextChange = e => {
this.setState({ msg: e.target.value });
};
// Function for sending message to chat server
onMessageSubmit = () => {
socket.emit("chat message", this.state.msg);
this.setState({ msg: "" });
};
render() {
return (
<div>
<input onChange={e => this.onTextChange(e)} value={this.state.msg} />
<button onClick={this.onMessageSubmit}>Send</button>
</div>
);
}
}
export default App;
I commented details on the code above. Firstly, we will change our App component to class based component in order to use state.
Then we add input and button for chat message. Add onTextChange and OnMessageSubmit to handle and send message.
Let’s test it. Change directory to server and “npm run dev”. When server is ready, go to “localhost:3000” and type something inside the input form and press send button.
cNd0Cv5-Q8BkhYI_AAAB: hi there
You can see message on your terminal.
Broadcasting Messages
Next step is broadcasting of messages. Broadcasting means sending received messages to all users in the same network.
To broadcast messages, we need to modify both server-side and client-side codes.
First, let’s start from server.
server/index.js
io.on("connection", socket => {
const { id } = socket.client;
console.log(`User connected: ${id}`);
socket.on("chat message", msg => {
// console.log(`${id}: ${msg}`);
io.emit("chat message", msg);
});
});
“io.emit” is a magical spell that broadcasts messages to all users.
Move to client directory.
Change constructor:
constructor() {
super();
this.state = { msg: "", chat: [] };
}
Add chat to state.
Then, add componentDidMount function to get messages from server.
componentDidMount() {
socket.on("chat message", ({ id, msg }) => {
// Add new messages to existing messages in "chat"
this.setState({
chat: [...this.state.chat, { id, msg }]
});
});
}
Create new function called “renderChat”. This will help rendering messages contained in “this.state.chat” array.
renderChat() {
const { chat } = this.state;
return chat.map(({ id, msg }, idx) => (
<div key={idx}>
<span style={{ color: "green" }}>{id}: </span>
<span>{msg}</span>
</div>
));
}
I made id to be green to distinguish it from messages. Notice that div tag has a key prop. Whenever map things in array to generate jsx tags, you need to provide key prop. Luckily, Array.prototype.map offers second param that offers index. We will use that index as a key.
Then add “renderChat” function to our jsx component.
<div>
<input onChange={e => this.onTextChange(e)} value={this.state.msg} />
<button onClick="{this.onMessageSubmit}">Send</button>
<div>{this.renderChat()}</div>
</div>
The whole code looks like:
client/src/App.js
import React, { Component } from "react";
import io from "socket.io-client";
const socket = io.connect("http://localhost:5000");
class App extends Component {
constructor() {
super();
this.state = { msg: "", chat: [] };
}
componentDidMount() {
socket.on("chat message", ({ id, msg }) => {
this.setState({
chat: [...this.state.chat, { id, msg }]
});
});
}
onTextChange = e => {
this.setState({ msg: e.target.value });
};
onMessageSubmit = () => {
socket.emit("chat message", this.state.msg);
this.setState({ msg: "" });
};
renderChat() {
const { chat } = this.state;
return chat.map(({ id, msg }, idx) => (
<div key={idx}>
<span style={{ color: "green" }}>{id}: </span>
<span>{msg}</span>
</div>
));
}
render() {
return (
<div>
<input onChange={e => this.onTextChange(e)} value={this.state.msg} />
<button onClick={this.onMessageSubmit}>Send</button>
<div>{this.renderChat()}</div>
</div>
);
}
}
export default App;
Let’s run our app again. Whenever you type message and press send button, you can see the message on the page with green-colored id.
Using Nicknames
Isn’t it annoying to see “xMhBSuo20CaN2HBRAAAD” or “Aso4wXNOH31vgt1sAAAG” instead of nicknames of users?
Last step. Let’s provide nicknames. Let’s add another input to get nicknames from the users.
This time, client-side first.
constructor() {
super();
this.state = { msg: "", chat: [], nickname: "" };
}
Add nickname to state.
componentDidMount() {
socket.on("chat message", ({ nickname, msg }) => {
this.setState({
chat: [...this.state.chat, { nickname, msg }]
});
});
}
Change “msg” param to “{ nickname, msg }”.
onTextChange = e => {
this.setState({ [e.target.name]: e.target.value });
};
Change “onTextChange” function to handle both nickname change and message change. Possible “e.target.name” could be “nickname” and “msg”.
onMessageSubmit = () => {
const { nickname, msg } = this.state;
socket.emit("chat message", { nickname, msg });
this.setState({ msg: "" });
};
Change “onMessageSubmit” function to send nickname with message. Careful that we don’t erase nickname after sending messages.
renderChat() {
const { chat } = this.state;
return chat.map(({ nickname, msg }, idx) => (
<div key={idx}>
<span style={{ color: "green" }}>{nickname}: </span>
<span>{msg}</span>
</div>
));
}
Change “id” to “nickname”.
<div>
<span>Nickname</span>
<input
name="nickname"
onChange={e => this.onTextChange(e)}
value={this.state.nickname}
/>
<span>Message</span>
<input
name="msg"
onChange={e => this.onTextChange(e)}
value={this.state.msg}
/>
<button onClick={this.onMessageSubmit}>Send</button>
<div>{this.renderChat()}</div>
</div>
Add span tags to distinguish nickname field from message field.
Full code looks like: client/src/App.js
import React, { Component } from "react";
import io from "socket.io-client";
const socket = io.connect("http://localhost:5000");
class App extends Component {
constructor() {
super();
this.state = { msg: "", chat: [], nickname: "" };
}
componentDidMount() {
socket.on("chat message", ({ nickname, msg }) => {
this.setState({
chat: [...this.state.chat, { nickname, msg }]
});
});
}
onTextChange = e => {
this.setState({ [e.target.name]: e.target.value });
};
onMessageSubmit = () => {
const { nickname, msg } = this.state;
socket.emit("chat message", { nickname, msg });
this.setState({ msg: "" });
};
renderChat() {
const { chat } = this.state;
return chat.map(({ nickname, msg }, idx) => (
<div key={idx}>
<span style={{ color: "green" }}>{nickname}: </span>
<span>{msg}</span>
</div>
));
}
render() {
return (
<div>
<span>Nickname</span>
<input
name="nickname"
onChange={e => this.onTextChange(e)}
value={this.state.nickname}
/>
<span>Message</span>
<input
name="msg"
onChange={e => this.onTextChange(e)}
value={this.state.msg}
/>
<button onClick={this.onMessageSubmit}>Send</button>
<div>{this.renderChat()}</div>
</div>
);
}
}
export default App;
Let’ move to server-side.
io.on("connection", socket => {
const { id } = socket.client;
console.log(`User Connected: ${id}`);
socket.on("chat message", ({ nickname, msg }) => {
io.emit("chat message", { nickname, msg });
});
});
Add nickname to param object. Make server to broadcast message with nickname.
Entire code: server/index.js
const express = require("express");
const app = express();
const server = require("http").Server(app);
const io = require("socket.io")(server);
io.on("connection", socket => {
const { id } = socket.client;
console.log(`User Connected: ${id}`);
socket.on("chat message", ({ nickname, msg }) => {
io.emit("chat message", { nickname, msg });
});
});
const PORT = process.env.PORT || 5000;
server.listen(PORT, () => console.log(`Listen on *: ${PORT}`));
Let’s run our app. Now we can send messages between users with own nicknames. Cool, isn’t it?
Yeah, it’s not beautiful. We definitely need to add some style but, it works! That’s all that matters.