Usando socket.io en aplicaciones de Next.js

Educación online efectiva

Aprende con expertos de la industria

COMPARTE ESTE ARTÍCULO Y MUESTRA LO QUE APRENDISTE

Next.js nos permite crear aplicaciones de React fácilmente y socket.io nos permite crear aplicaciones en tiempo real fácilmente ¿Qué mejor que combinarlos para crear aplicaciones de React en tiempo real fácilmente?

En este artículo vamos a ver cómo combinarlos para crear una aplicación que use ambas tecnologías para crear una app de chat simple, que muestre los mensajes antiguos al entrar a la página y luego mediante socket.io nos permita enviar y recibir mensajes.

Iniciando el proyecto

Lo primero, como siempre, es iniciar el proyecto.

npm init --yes
# o con yarn
yarn init --yes

Luego vamos a instalar nuestras dependencias:

npm i next react react-dom express socket.io socket.io-client isomorphic-fetch
# o con yarn
yarn add next react react-dom express socket.io socket.io-client isomorphic-fetch

Y vamos a agregar estos scripts a nuestro package.json:

{
	...
	"scripts": {
		"dev": "next",
		"build": "next build",
		"start": "next start"
	}
	...
}

Creando el servidor de API y Sockets

Vamos a crear un simple API Rest con un endpoint y un servidor de sockets. Este endpoints del API nos van a permitir pedir nuestros mensajes viejos. La parte de sockets nos permite enterarnos cuando un usuario crea un mensaje y crear mensajes.

Por simplicidad del ejemplo, vamos a meter todo en un solo servidor en el que también vamos a correr Next.js. En un proyecto real deberíamos tener un servidor para el API Rest, un servidor para sockets y un servidor para Next.js, de forma que puedan escalar independientemente.

// cargamos express e iniciamos una aplicación
const app = require('express')()
// creamos un servidor HTTP desde de nuestra aplicación de Express
const server = require('http').Server(app)
// creamos una aplicación de socket.io desde nuestro servidor HTTP
const io = require('socket.io')(server)
// cargamos Next.js
const next = require('next')

// verificamos si estamos corriendo en desarrollo o producción
const dev = process.env.NODE_ENV !== 'production'
// iniciamos nuestra aplicación de Next.js
const nextApp = next({ dev })
// obtenemos el manejador de Next.js
const nextHandler = nextApp.getRequestHandler()

// este array va a ser nuestra base de datos
// no es una base de datos de verdad, pero para el ejemplo nos sirve
const messages = []

// cuando un usuario se conecte al servidor de sockets
io.on('connection', socket => {
	// escuchamos el evento `message`
	socket.on('message', (data) => {
		// guardamos el mensaje en nuestra "DB"
		messages.push(data)
		// enviamos el mensaje a todos los usuarios menos a quién los envió
		socket.broadcast.emit('message', data)
	})
})

// iniciamos nuestra aplicación de Next.js
nextApp.prepare().then(() => {
	// definimos una URL para obtener los mensajes
	app.get('/messages', (req, res) => {
		// y respondemos con la lista de mensajes serializada como JSON
		res.json(messages)
	})

	// para cualquier otra ruta de la aplicación
	app.get('*', (req, res) => {
		// dejamos que el manejador de Next se encargue y responda con el HTML o un 404
		return nextHandler(req, res)
	})

	// iniciamos el servidor HTTP en el puerto 3000
	server.listen(3000, (err) => {
		// si ocurre un error matamos el proceso
		if (err) process.exit(0)
		// si todo está bien dejamos un log en consola
		console.log('> Ready on http://localhost:3000')
	})
})

Ese es nuestro servidor, como vemos usamos un array en memoria para guardar los mensajes por simplicidad del ejemplo, en la vida real deberíamos tener una base de datos (laquesea) y guardar los mensajes ahí.

También tenemos un muy simple servidor de sockets y una URL donde pedir los mensajes guardados en el servidor.

Ahora vamos a hacer el frontend. Para eso creamos una página de Next.js en pages/index.js. Esta página va a pedir los mensajes viejos en el método getInitialProps y luego cuando se renderice en el navegador va a conectarse al servidor de sockets para enviar y recibir nuevos mensajes.

// importamos Component de React
import { Component } from'react'
// importamos el client de socket.io
import io from'socket.io-client'
// importamos fetch
import fetch from'isomorphic-fetch'

classHomePageextendsComponent{
	// acá pedimos los datos de los mensajes viejos, esto se ejecuta tanto en el cliente como en el servidor
	staticasync getInitialProps ({ req }) {
		const response = await fetch('http://localhost:3000/messages')
		const messages = await response.json()
		return { messages }
	}

	static defaultProps = {
		messages: []
	}

	// en el estado guardamos un string vacío (el campo del formulario) y los mensajes que recibimos del API
	state = {
		field: '',
		messages: this.props.messages
	}

	// una vez que el componente se montó en el navegador nos conectamos al servidor de sockets
	// y empezamos a recibimos el evento `message` del servidor
	componentDidMount () {
		this.socket = io('http://localhost:3000/')
		this.socket.on('message', this.handleMessage)
	}

	// cuando el componente se va a desmontar es importante que dejemos de escuchar el evento
	// y que cerremos la conexión por sockets, esto es para evitar problemas de que lleguen mensajes
	componentWillUnmount () {
		this.socket.off('message', this.handleMessage)
		this.socket.close()
	}

	// cuando llega un mensaje del servidor lo agregamos al estado de nuestra página
	handleMessage = (message) => {
		this.setState(state => ({ messages: state.messages.concat(message) }))
	}

	// cuando el valor del input cambia actualizamos el estado de nuestra página
	handleChange = event => {
		this.setState({ field: event.target.value })
	}

	// cuando se envía el formulario enviamos el mensaje al servidor
	handleSubmit = event => {
		event.preventDefault()

		// creamos un objeto message con la fecha actual como ID y el valor del input
		const message = {
			id: (newDate()).getTime(),
			value: this.state.field
		}

		// enviamos el objeto por socket al servidor
		this.socket.emit('message', message)

		// lo agregamos a nuestro estado para que se muestre en pantalla y limpiamos el input
		this.setState(state => ({
			field: '',
			messages: state.messages.concat(message)
		}))
	}

	render () {
		return (
			<main>
				<div>
					<ul>
						{/* acá renderizamos cada mensaje */}
						{this.state.messages.map(message =>
							<likey={message.id}>{message.value}</<span class="hljs-name">li>
						)}
					</<span class="hljs-name">ul>
					{/* nuestro formulario *}
					<formonSubmit={this.handleSubmit}>
						<input
							onChange={this.handleChange}
							type='text'
							placeholder='Hola Platzi!'
							value={this.state.field}
						/>
						<button>Enviar</<span class="hljs-name">button>
					</<span class="hljs-name">form>
				</<span class="hljs-name">div>
			</<span class="hljs-name">main>
		)
	}
}

export default HomePage

Con eso ya tenemos nuestra aplicación hecha. Es importante ver que la conexión a socket solo la hacemos en el cliente, por eso lo hacemos en componentDidMount y no en componentWillMount. Si lo hiciéramos en WillMount o en getInitialProps como se ejecuta en el servidor, entonces tendríamos el problema que se inicia una conexión pero nunca se cierra, lo que puede causar problema de quedarnos sin memoria en nuestro servidor.

Pueden ver este mismo ejemplo funcionando en => https://next-socket-io.now.sh/

Conclusiones

Combinar socket.io y Next.js es muy fácil: hacer la petición HTTP para el server render nos ayuda a mejorar la experiencia de carga de la página, ya que podemos mostrar mensajes desde el primer momento incluso si luego la conexión por socket no funciona.

Se pueden hacer validaciones más complejas verificando si estamos de verdad conectados al servidor de socket y ahí deshabilitar o habilitar el formulario para que no se pueda usar si no estamos conectados, por ejemplo en el server render.

Educación online efectiva

Aprende con expertos de la industria

COMPARTE ESTE ARTÍCULO Y MUESTRA LO QUE APRENDISTE

0 Comentarios

para escribir tu comentario

Artículos relacionados