Cómo crear una aplicación de chat React simple usando Socket.IO

Publicado: 2022-02-10

Oye, ¿alguna vez te has preguntado cómo funcionan las aplicaciones como Slack? ¿O sobre lo difícil que sería crear una aplicación como esa?

En este artículo, le mostraremos una guía paso a paso sobre cómo crear una aplicación de chat de reacción simple similar a Slack usando ReactJS y SocketIO. Construiremos una versión bastante simplificada de todas las características que Slack tiene para ofrecer, así que tome este tutorial como un buen ejemplo inicial.

Antes de entrar en el trabajo de desarrollo esencial, hay algunos elementos esenciales que debe tener listos.

Los 3 requisitos previos clave:

  1. Necesitaría tener conocimientos básicos de JavaScript.
  2. Debe tener NodeJS y NPM instalados en su dispositivo.
  3. Tener un IDE, o cualquier editor de texto preferible.

Una vez que los haya configurado, seguiremos los pasos para lograr una aplicación que tenga 3 características muy simples:

  1. Inicie sesión proporcionando un apodo.
  2. Cambiar entre canales proporcionados estáticamente.
  3. Envía mensajes a los canales (incl. emoji).

Cuando hayamos terminado, debería tener una aplicación que se vea así:

Inicio de sesión y principal de la aplicación de chat

¿Tienes todo listo? ¿¡Sí!? Vamos a ello entonces, vamos a...

1. Inicializar la aplicación ReactJS

Primero, necesitamos crear e inicializar la aplicación ReactJS. Para eso usaremos create-react-app.

Abre tu terminal y ejecuta:
npx create-react-app simple-react-js-chat-application

Esto creará un nuevo directorio simple-react-js-chat-application con el esqueleto base de ReactJS. No revisaremos la estructura del proyecto base en este momento.

2. Instalar dependencias

El siguiente paso es instalar las dependencias necesarias para nuestro cliente front-end. En su terminal:

  • Ir al directorio del proyecto:
    cd simple-react-js-chat-application
  • Correr:
    npm install axios emoji-mart node-sass skeleton-css socket.io-client uuid

Esto instalará las dependencias de requisitos previos:

  • axios : lo estamos usando para hacer llamadas al back-end para obtener canales y mensajes.
  • emoji-mart : es el componente React para emojis.
  • skeleton-css : un repetitivo CSS receptivo simple.
  • socket.io-client : paquete NPM para conectarse al socket.
  • uuid : biblioteca de identificación de usuario única
  • node-sass : usaremos SCSS.

3. Cree el servidor back-end

Para usar Socket.IO, necesitamos crear un servidor que maneje los eventos y algunos de los puntos finales de la API, es decir, recuperar canales y mensajes. En este caso, usaremos un servidor lo más simple posible manejado en NodeJS.

Comience con la creación de un nuevo servidor de directorio en la carpeta src . Luego comience a crear los siguientes archivos:

Un archivo Package.json

El archivo package.json especifica el manejo, las dependencias y las dependencias de desarrollo de npm. Es un archivo JSON real, no un objeto JavaScript.

Los campos principales que necesita Node.JS son el nombre y la versión. El nombre contiene el nombre y la versión del proyecto: la versión del paquete.

 { "name": "server", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "cors": "^2.8.5", "express": "^4.17.1", "socket.io": "^3.0.4", "uuid": "^8.3.2" } }

Un archivo Server.js

Este archivo sigue la lógica de que el servidor back-end maneja la creación de instancias del servidor, las rutas personalizadas y los detectores de eventos/emisión.

 const http = require("http"); const express = require("express"); const cors = require("cors"); const socketIO = require("socket.io"); const { addMessage, getChannelMessages } = require("./messages"); const { channels, addUserToChannel } = require("./channels"); const { addUser, removeUser } = require("./users"); const app = express(); app.use(cors()); const server = http.createServer(app); const io = socketIO(server, { cors: { origin: "*", }, }); const PORT = process.env.PORT || 8080; io.on("connection", (socket) => { // Get nickname and channel. const { nickname, channel } = socket.handshake.query; console.log(`${nickname} connected`); // Join the user to the channel. socket.join(channel); addUser(nickname, socket.id); addUserToChannel(channel, nickname); // Handle disconnect socket.on("disconnect", () => { console.log(`${nickname} disconnected`); removeUser(nickname); }); socket.on("CHANNEL_SWITCH", (data) => { const { prevChannel, channel } = data; if (prevChannel) { socket.leave(prevChannel); } if (channel) { socket.join(channel); } }); socket.on("MESSAGE_SEND", (data) => { addMessage(data); const { channel } = data; socket.broadcast.to(channel).emit("NEW_MESSAGE", data); }); }); app.get("/channels/:channel/messages", (req, res) => { const allMessages = getChannelMessages(req.params.channel); return res.json({ allMessages }); }); app.get("/getChannels", (req, res) => { return res.json({ channels }); }); server.listen(PORT, () => console.log(`Server listening to port ${PORT}`));

Un archivo Users.js

Este componente es el responsable de los “usuarios” de la aplicación que estamos construyendo. En este caso, solo tenemos que agregar/eliminar un usuario.

 const users = {}; const addUser = (nickname, socketId) => { users[nickname] = socketId; } const removeUser = (nickname) => { if(users.hasOwnProperty(nickname)) { delete users[nickname]; } } module.exports = { users, addUser, removeUser };

Un archivo Messages.js

Este archivo agrega la funcionalidad para los mensajes, es decir, agregarlos a la matriz y obtener mensajes de canales específicos.

 const messages = []; const addMessage = (data) => { messages.push(data); return data; }; const getChannelMessages = (channel) => messages.filter((message) => message.channel === channel); module.exports = { addMessage, getChannelMessages };

Un archivo Channels.js

Este archivo contiene la lógica de los canales: inicializa los predeterminados y una funcionalidad para agregar usuarios a un canal.

 const channels = [ { id: 1, name: "general", users: [], }, { id: 2, name: "test 1", users: [], }, { id: 3, name: "test 2", users: [], }, ]; const addUserToChannel = (channel, nickname) => { channels.filter((c) => c.name === channel).map((c) => { c.users.push(nickname); return c; }); } module.exports = { channels, addUserToChannel };

Ejecute la terminal y en el directorio instale las dependencias de NPM:

 npm run install

Espere a que NPM termine y el servidor esté listo. Puedes probarlo ejecutando

 npm start

Servidor de aplicación de chat iniciado

4. Crear la parte frontal

Lo último en orden, pero no en importancia, es crear la parte frontal de la aplicación. El front-end se comunicará con el servidor back-end para proporcionar las funciones principales, es decir, ingresar apodos, cambiar entre canales y enviar mensajes.

Comencemos con la pantalla inicial de la aplicación.
Navegue a la carpeta src y abra App.js. Luego reemplace su contenido con el siguiente:

 import "./index.css"; import "./App.css"; import { useState } from "react"; import Chat from "./components/Chat/Chat"; import LoginDialog from "./components/LoginDialog"; function App() { const [nickname, setNickname] = useState(""); const [loggedIn, setLoggedIn] = useState(false); const handleNicknameChange = (event) => { setNickname(event.target.value.trim()); }; const handleNicknameSubmit = (e) => { if (!nickname.length) return; e.preventDefault(); setLoggedIn(true); }; return ( <div className="main-div"> {!loggedIn ? ( <LoginDialog nicknameChange={handleNicknameChange} nicknameSubmit={handleNicknameSubmit} /> ) : ( <Chat nickname={nickname} /> )} </div> ); } export default App;

El componente de la aplicación contiene la lógica para el estado del apodo y si un usuario está "iniciado sesión". También representa el componente apropiado, ya sea LoginDialog o Chat, que depende del estado.

Añadir algo de forma

Abra App.css y reemplace el contenido con:

 .login-container { position: fixed; left: 10%; right: 10%; top: 50%; transform: translate(0, -50%); display: flex; flex-direction: column; } .text-input-field { padding: 24px 12px; border-radius: 7px; font-size: 24px; } .text-input-field:focus { outline: none; } .login-button { margin-top: 20px; padding: 24px 12px; font-size: 28px; background-color: rgb(0, 132, 255); color: white; font-weight: 600; text-align: center; text-decoration: none; border-radius: 7px; }

Cree nuevos asistentes de carpetas y coloque un archivo con el nombre socket.js dentro .

 import io from "socket.io-client"; import axios from "axios"; let socket; const SOCKET_URL = "http://localhost:8080"; export const initiateSocket = (channel, nickname) => { socket = io(SOCKET_URL, { query: { channel, nickname }, }); console.log("Connecting to socket"); if (socket && channel) { socket.emit("CHANNEL_JOIN", channel); } }; export const switchChannel = (prevChannel, channel) => { if (socket) { socket.emit("CHANNEL_SWITCH", { prevChannel, channel }); } }; export const subscribeToMessages = (callback) => { if (!socket) { return; } socket.on("NEW_MESSAGE", (data) => { callback(null, data); }); }; export const sendMessage = (data) => { if (!socket) { return; } socket.emit("MESSAGE_SEND", data); }; export const fetchChannels = async () => { const response = await axios.get(`${SOCKET_URL}/getChannels`); return response.data.channels; }; export const fetchChannelMessages = async (channel) => { const response = await axios.get( `${SOCKET_URL}/channels/${channel}/messages` ); return response.data.allMessages; };

Este componente exporta las funciones auxiliares necesarias que usaremos más adelante en los componentes de React para comunicarse con el servidor back-end.

¡Ya casi estamos listos!
Ahora continuaremos con la creación del cuadro de diálogo para proporcionar el apodo y el diseño del chat.

Configurar el inicio de sesión

Cree nuevos componentes de carpeta y continuemos con la creación de los componentes necesarios para la aplicación.

LoginDialog.js:

 function LoginDialog({ nicknameChange, nicknameSubmit }) { return ( <div className="dialog-container"> <div className="dialog"> <form className="dialog-form" onSubmit={nicknameSubmit}> <label className="username-label" htmlFor="username"> Nickname: </label> <input className="username-input" autoFocus onChange={nicknameChange} type="text" name="userId" placeholder="Enter your nickname to continue" /> <button type="submit" className="submit-btn"> Continue </button> </form> </div> </div> ); } export default LoginDialog;

Este es el componente de la pantalla de inicio de sesión que se abre cuando la aplicación se carga inicialmente. En este punto no se proporciona ningún apodo. Contiene solo marcas y nada relacionado con el estado aquí. Como parece del código, el estado y los controladores se pasan a través de accesorios.

Dale vida al chat

Continúe con la creación de una carpeta más: Chat , en la que crearemos varios componentes:

Chat.js

 import { useEffect, useRef, useState } from "react"; import "skeleton-css/css/normalize.css"; import "skeleton-css/css/skeleton.css"; import "./Chat.scss"; import { initiateSocket, switchChannel, fetchChannels, fetchChannelMessages, sendMessage, subscribeToMessages, } from "../../helpers/socket"; import { v4 as uuidv4 } from "uuid"; import "emoji-mart/css/emoji-mart.css"; import Channels from "./Channels"; import ChatScreen from "./ChatScreen"; function Chat({ nickname }) { const [message, setMessage] = useState(""); const [channel, setChannel] = useState("general"); const [channels, setChannels] = useState([]); const [messages, setMessages] = useState([]); const [messagesLoading, setMessagesLoading] = useState(true); const [channelsLoading, setChannelsLoading] = useState(true); const [showEmojiPicker, setShowEmojiPicker] = useState(false); const prevChannelRef = useRef(); useEffect(() => { prevChannelRef.current = channel; }); const prevChannel = prevChannelRef.current; useEffect(() => { if (prevChannel && channel) { switchChannel(prevChannel, channel); setChannel(channel); } else if (channel) { initiateSocket(channel, nickname); } }, [channel]); useEffect(() => { setMessages([]); setMessagesLoading(true); fetchChannelMessages(channel).then((res) => { setMessages(res); setMessagesLoading(false); }); }, [channel]); useEffect(() => { fetchChannels().then((res) => { setChannels(res); setChannelsLoading(false); }); subscribeToMessages((err, data) => { setMessages((messages) => [...messages, data]); }); }, []); const handleMessageChange = (event) => { setMessage(event.target.value); }; const handleMessageSend = (e) => { if (!message) return; e.preventDefault(); const data = { id: uuidv4(), channel, user: nickname, body: message, time: Date.now(), }; setMessages((messages) => [...messages, data]); sendMessage(data); setMessage(""); }; const handleEmojiSelect = (emoji) => { const newText = `${message}${emoji.native}`; setMessage(newText); setShowEmojiPicker(false); }; return ( <div className="chat-container"> <Channels nickname={nickname} channelsLoading={channelsLoading} channels={channels} }channel={channel} setChannel={setChannel} /> <ChatScreen channel={channel} messagesLoading={messagesLoading} messages={messages} showEmojiPicker={showEmojiPicker} handleEmojiSelect={handleEmojiSelect} handleMessageSend={handleMessageSend} setShowEmojiPicker={setShowEmojiPicker} message={message} handleMessageChange={handleMessageChange} /> </div> ); export default Chat;

Este es el componente principal que sostiene la lógica de la aplicación de chat, cuando un usuario está "iniciado sesión". Muestra los controladores y estados de los canales, mensajes y envío de mensajes.

Dale estilo

Continuemos con los estilos para el componente y sus componentes secundarios:

Chat.scss

 .chat-container { width: 100vw; height: 100vh; display: grid; grid-template-columns: 1fr 4fr; } .right-sidebar { border-left: 1px solid #ccc; } .left-sidebar { border-right: 1px solid #ccc; } .user-profile { height: 70px; display: flex; align-items: flex-start; padding-right: 20px; padding-left: 20px; justify-content: center; flex-direction: column; border-bottom: 1px solid #ccc; } .user-profile span { display: block; } .user-profile .username { font-size: 20px; font-weight: 700; } .chat-channels li, .room-member { display: flex; align-items: center; padding: 15px 20px; font-size: 18px; color: #181919; cursor: pointer; border-bottom: 1px solid #eee; margin-bottom: 0; } .room-member { justify-content: space-between; padding: 0 20px; height: 60px; } .send-dm { opacity: 0; pointer-events: none; font-size: 20px; border: 1px solid #eee; border-radius: 5px; margin-bottom: 0; padding: 0 10px; line-height: 1.4; height: auto; } .room-member:hover .send-dm { opacity: 1; pointer-events: all; } .presence { display: inline-block; width: 10px; height: 10px; background-color: #ccc; margin-right: 10px; border-radius: 50%; } .presence.online { background-color: green; } .chat-channels .active { background-color: #eee; color: #181919; } .chat-channels li:hover { background-color: #d8d1d1; } .room-icon { display: inline-block; margin-right: 10px; } .chat-screen { display: flex; flex-direction: column; height: 100vh; } .chat-header { height: 70px; flex-shrink: 0; border-bottom: 1px solid #ccc; padding-left: 10px; padding-right: 20px; display: flex; flex-direction: column; justify-content: center; } .chat-header h3 { margin-bottom: 0; text-align: center; } .chat-messages { flex-grow: 1; overflow-y: auto; display: flex; flex-direction: column; justify-content: flex-end; margin-bottom: 0; min-height: min-content; position: relative; } .message { padding-left: 20px; padding-right: 20px; margin-bottom: 10px; display: flex; justify-content: space-between; align-items: center; } .message span { display: block; text-align: left; } .message .user-id { font-weight: bold; } .message-form { border-top: 1px solid #ccc; width: 100%; display: flex; align-items: center; } .message-form, .message-input { width: 100%; margin-bottom: 0; } .rta { flex-grow: 1; } .emoji-mart { position: absolute; bottom: 20px; right: 10px; } input[type="text"].message-input, textarea.message-input { height: 50px; flex-grow: 1; line-height: 35px; padding-left: 20px; border-radius: 0; border-top-left-radius: 0; border-top-right-radius: 0; border-bottom-left-radius: 0; border-bottom-right-radius: 0; border: none; font-size: 16px; color: #333; min-height: auto; overflow-y: hidden; resize: none; border-left: 1px solid #ccc; } .message-input:focus { outline: none; } .toggle-emoji { border: none; width: 50px; height: auto; padding: 0; margin-bottom: 0; display: flex; align-items: center; justify-content: center; } .toggle-emoji svg { width: 28px; height: 28px; } /* RTA ========================================================================== */ .rta { position: relative; border-left: 1px solid #ccc; display: flex; flex-direction: column; } .rta__autocomplete { position: absolute; width: 300px; background-color: white; border: 1px solid #ccc; border-radius: 5px; } .rta__autocomplete ul { list-style: none; text-align: left; margin-bottom: 0; } .rta__autocomplete li { margin-bottom: 5px; padding: 3px 20px; cursor: pointer; } .rta__autocomplete li:hover { background-color: skyblue; } /* Dialog ========================================================================== */ .dialog-container { position: absolute; top: 0; right: 0; bottom: 0; left: 0; background-color: rgba(0, 0, 0, 0.8); display: flex; justify-content: center; align-items: center; } .dialog { width: 500px; background-color: white; display: flex; align-items: center; } .dialog-form { width: 100%; margin-bottom: 0; padding: 20px; } .dialog-form > * { display: block; } .username-label { text-align: left; font-size: 16px; } .username-input { width: 100%; } input[type="text"]:focus { border-color: #5c8436; } .submit-btn { color: #5c8436; background-color: #181919; width: 100%; } .submit-btn:hover { color: #5c8436; background-color: #222; }

ChatMensajes.js

 import ChatMessages from "./ChatMessages"; import MessageForm from "./MessageForm"; function ChatScreen({ channel, messagesLoading, messages, showEmojiPicker, handleEmojiSelect, handleMessageSend, setShowEmojiPicker, message, handleMessageChange, }) { return ( <section className="chat-screen"> <header className="chat-header"> <h3>#{channel}</h3> </header> <ChatMessages messagesLoading={messagesLoading} messages={messages} /> <footer className="chat-footer"> <MessageForm emojiSelect={handleEmojiSelect} handleMessageSend={handleMessageSend} setShowEmojiPicker={setShowEmojiPicker} showEmojiPicker={showEmojiPicker} message={message} handleMessageChange={handleMessageChange} /> </footer> </section> ); } export default ChatScreen;

El componente principal de la pantalla de chat contiene los componentes ChatMessages y MessageForm.

MessageForm.js

 import { Smile } from "react-feather"; import { Picker } from "emoji-mart"; function MessageForm({ emojiSelect, handleMessageSend, setShowEmojiPicker, showEmojiPicker, message, handleMessageChange, }) { let messageInput; const handleEmojiSelect = (emoji) => { emojiSelect(emoji); messageInput.focus(); }; return ( <div> {showEmojiPicker ? ( <Picker title="" set="apple" onSelect={handleEmojiSelect} /> ) : null} <form onSubmit={handleMessageSend} className="message-form"> <button type="button" onClick={() => setShowEmojiPicker(!showEmojiPicker)} className="toggle-emoji" > <Smile /> </button> <input type="text" value={message} ref={(input) => (messageInput = input)} onChange={handleMessageChange} placeholder="Type your message here..." className="message-input" /> </form> </div> ); } export default MessageForm;

Contiene la lógica y la representación de los campos de entrada del mensaje: el selector de emoji y el cuadro de entrada del mensaje.

Crear la barra lateral

El último componente de la estructura de la aplicación contiene la "barra lateral", es decir, la lista de canales.

Canales.js

 import { useState } from "react"; function Channels({ nickname, channelsLoading, channels, channel, setChannel, }) { return ( <aside className="sidebar left-sidebar"> <div className="user-profile"> <span className="username">@ {nickname}</span> </div> <div className="channels"> <ul className="chat-channels"> {channelsLoading ? ( <li> <span className="channel-name">Loading channels....</span> </li> ) : channels.length ? ( channels.map((c) => { return ( <li key={c.id} onClick={() => setChannel(c.name)} className={c.name === channel ? "active" : ""} > <span className="channel-name">{c.name}</span> </li> ); }) ) : ( <li> <span className="channel-name">No channels available</span> </li> )} </ul> </div> </aside> ); } export default Channels;

La aplicación está lista, lo único que debe considerar aquí es verificar la URL de la API en helpers/socket.js :

 const SOCKET_URL = "http://localhost:8080";

Asegúrese de cambiarlo de acuerdo con la URL y el PUERTO para el servidor de back-end que está utilizando.

Ejecute tanto la parte frontal como el servidor:

Navegue al directorio raíz y ejecute:
npm start


Navegue a src/server y ejecute:
npm start

Ahora puede abrir http://localhost:3000 o cualquier otro puerto que use para la parte frontal y acceder a él.

Ten en cuenta que esta aplicación tiene características muy básicas. No hay autenticación de inicio de sesión: solo necesita un apodo. Y no podrá crear sus propios canales; solo podrá alternar entre los estáticos que se proporcionan.

¡Bien hecho!

Seguiste nuestra guía y ahora tienes tu propia aplicación de chat simplificada similar a Slack.

Siéntase libre de experimentar y ampliar la aplicación. Puedes agregar cosas como:

  • Una función de "usuario está escribiendo".
  • Una función para unirse/crear canales.
  • Autenticación.
  • Avatares de usuario.
  • Una capacidad para mostrar qué usuarios están activos en línea.

Háganos saber cómo le gusta. Y si necesita más ayuda, no dude en ponerse en contacto con nosotros.