Leon Chaewon Kong's dev blog

Simple Chat App - React with Socket.IO

Table of Contents

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.