STOMP es un protocolo increíblemente simple y potente para enviar mensajes implementado por servidores populares como RabbitMQ, ActiveMQ y Apollo. El uso de STOMP sobre WebSocket es un protocolo directo, lo que lo hace una opción popular para enviar mensajes desde un navegador web, ya que protocolos como AMQP están limitados por navegadores que no permiten conexiones TCP.

Para usar STOMP sobre WebSocket, puede utilizar @stomp/stompjs, pero este tiene callbacks complicados y una API complicada que atiende a casos de uso más especializados. Afortunadamente, también existe el menos conocido @stompjs/rx-stomp que proporciona una interfaz agradable a través de observables de RxJS. Los observables no son exclusivos de Angular y se adaptan muy bien a cómo funciona React. Es una interfaz interesante cuando se componen flujos de trabajo y pipelines complejos con muchas diferentes fuentes de mensajes.

El tutorial sigue una senda algo similar a la versión inicial en Angular, pero la estructura de componentes y el estilo de código se ajustan al estilo funcional de React.

Nota: Este tutorial está escrito con TypeScript strict, pero el código JavaScript es casi idéntico ya que solo tenemos 5 declaraciones de tipo. Para la versión JS, puede omitir las importaciones y definiciones de tipo.

Tabla de Contenido

Objetivos

Aquí, construiremos una aplicación de chat simplificada que muestra diferentes aspectos de RxStomp a través de diferentes componentes. En general, queremos tener:

  • Un frontend de React conectado con RxStomp a un servidor STOMP.

  • Un display de estado de conexión en vivo basado en nuestra conexión con el servidor STOMP.

  • Logica Pub/Sub para cualquier tema configurable.

  • Dividir la lógica de RxStomp a través de múltiples componentes para mostrar cómo separar la lógica y la responsabilidad.

  • Alinear los ciclos de vida de las conexiones/suscripciones RxStomp con los ciclos de vida de los componentes React para asegurar que no hay fallas o observadores sin cerrar.

Prerrequisitos

  • Debería tener un servidor STOMP en ejecución para que la aplicación React pueda conectar con él. En este caso, utilizaremos RabbitMQ con la extensión rabbitmq_web_stomp.

  • Última versión de React. En este tutorial se utilizará la versión v18, aunque problablemente funcionen versiones más antiguas.

  • También resultará útil tener familiaridad con los observables.

Servidor STOMP de inicio con RabbitMQ

Si también desea utilizar RabbitMQ (no obligatorio), aquí tienes guías de instalación para diferentes sistemas operativos. Para agregar la extensión, tendrás que ejecutar:

$ rabbitmq-plugins enable rabbitmq_web_stomp

Si puedes usar Docker, un archivo Docker similar a este configurará todo lo necesario para el tutorial:

FROM rabbitmq:3.8.8-alpine

run rabbitmq-plugins enable --offline rabbitmq_web_stomp

EXPOSE 15674

Plantilla de Inicio de React

Para este tutorial, utilizaremos la plantilla Vite `react-ts`. La parte central de nuestra aplicación estará en el componente `App` y crearemos componentes hijos para otras funcionalidades específicas de STOMP.

Cómo instalar RxStomp

Usaremos el paquete npm @stomp/rx-stomp:

$ npm i @stomp/rx-stomp rxjs

Esto instalará la versión 2.0.0

Nota: Este tutorial aún funciona sin especificar explícitamente rxjs ya que es una dependencia hermana, pero es buena práctica ser explícito al respecto.

Cómo gestionar la conexión y desconexión con el servidor STOMP

Ahora, abramos App.tsx y inicializamos nuestro cliente RxStomp. Como el cliente no es un estado que cambiará para la renderización, lo wrapeamos en el Hook `useRef`.

// src/App.tsx
import { useRef } from 'react'
import { RxStomp } from '@stomp/rx-stomp'

import './App.css'

function App() {
  const rxStompRef = useRef(new RxStomp())
  const rxStomp = rxStompRef.current

  return (
    <>
      <h1>Hello RxStomp!</h1>
    </>
  )
}

export default App

Asumiendo los puertos y detalles de autenticación predeterminados, definiremos una configuración para nuestra conexión a continuación.

// src/App.tsx

import { RxStomp } from '@stomp/rx-stomp'
import type { RxStompConfig } from '@stomp/rx-stomp'
...
const rxStompConfig: RxStompConfig = {
  brokerURL: 'ws://localhost:15674/ws',
  connectHeaders: {
    login: 'guest',
    passcode: 'guest',
  },
  debug: (msg) => {
    console.log(new Date(), msg)
  },
  heartbeatIncoming: 0,
  heartbeatOutgoing: 20000,
  reconnectDelay: 200,
}

function App() {
  ...

Para un mejor experiencia de desarrollo, hemos registrado todos los mensajes con fechas y horas en un consola local y establecimos frecuencias de temporizador bajas. Su configuración para su aplicación de producción debería ser muy diferente, así que revise las documentaciones de RxStompConfig para todas las opciones disponibles.

A continuación, pasaremos la configuración a rxStomp dentro de un Hook useEffect. Este maneja la activación de la conexión junto con el ciclo de vida del componente.

// src/App.tsx
...
function App() {
  const rxStompRef = useRef(new RxStomp())
  const rxStomp = rxStompRef.current

  useEffect(() => {
    rxStomp.configure(rxStompConfig)
    rxStomp.activate()

    return () => { 
      rxStomp.deactivate() 
    }
  })
  ...

Aunque no hay cambio visual en nuestra app, la comprobación de los registros debería mostrar los registros de conexión y ping. Aquí tienen un ejemplo de cómo debe verse:

Date ... >>> CONNECT
login:guest
passcode:guest
accept-version:1.2,1.1,1.0
heart-beat:20000,0

Date ... Received data 
Date ... <<< CONNECTED
version:1.2
heart-beat:0,20000
session:session-EJqaGQijDXqlfc0eZomOqQ
server:RabbitMQ/4.0.2
content-length:0

Date ... connected to server RabbitMQ/4.0.2 
Date ... send PING every 20000ms 
Date ... <<< PONG 
Date ... >>> PING

Nota: En general, si ven registros duplicados, puede ser un signo de que no se implementó correctamente la funcionalidad de desactivación o descarga. React renderiza cada componente dos veces en un entorno de desarrollo para ayudar a las personas a atrapar estos errores mediante React.StrictMode

Cómo supervisar el estado de la conexión

RxStomp tiene un enum RxStompState que representa los posibles estados de conexión con nuestro broker. Nuestro próximo objetivo es mostrar el estado de conexión en nuestra UI.

Vamos a crear un componente nuevo para esto llamado Status.tsx:

// src/Status.tsx
import { useState } from 'react'

export default function Status() {
  const [connectionStatus, setConnectionStatus] = useState('')

  return (
    <>
      <h2>Connection Status: {connectionStatus}</h2>
    </>
  )
}

Podemos usar el observable rxStomp.connectionState$ para enlazar a nuestra cadena de caracteres connectionStatus. Similar a cómo usamos useEffect, usaremos la acción de desmontaje para unsubscribe().

// src/Status.tsx
import { RxStompState } from '@stomp/rx-stomp'
import { useEffect, useState } from 'react'
import type { RxStomp } from '@stomp/rx-stomp'


export default function Status(props: { rxStomp: RxStomp }) {
  const [connectionStatus, setConnectionStatus] = useState('')

  useEffect(() => {
    const statusSubscription = props.rxStomp.connectionState$.subscribe((state) => {
      setConnectionStatus(RxStompState[state])
    })

    return () => {
      statusSubscription.unsubscribe()
    }
  }, [])

  return (
    <>
      <h2>Connection Status: {connectionStatus}</h2>
    </>
  )
}

Para verlo, lo incluimos en nuestra app:

// src/App.tsx
import Status from './Status'
...
  return (
    <>
      <h1>Hello RxStomp!</h1>

      <Status rxStomp={rxStomp}/>
    </>
  )

En este punto, debería tener un indicador visual funcional en la pantalla. Pruebe jugando con apagar el servidor STOMP y ver si los registros funcionan como se espera.

Cómo Enviar Mensajes

Vamos a crear un chatroom simple para mostrar un flujo de mensajería end-to-end simplificado con el broker.

Podemos colocar la funcionalidad en un nuevo componente Chatroom. Primero, podemos crear el componente con un campo personalizado username y message que está enlazado a entradas.

// src/Chatroom.tsx
import { useState } from 'react'
import type { RxStomp } from '@stomp/rx-stomp'

export default function Chatroom(props: {rxStomp: RxStomp}) {
  const [message, setMessage] = useState('')
  const [userName, setUserName] = useState(`user${Math.floor(Math.random() * 1000)}`)

  return (
    <>
      <h2>Chatroom</h2>

      <label htmlFor='username'>Username: </label>
      <input
        type='text'
        name='username'
        value={userName}
        onChange={(e) => setUserName(e.target.value)}
      />

      <label htmlFor='message'>Message: </label>

      <input
        type='text'
        value={message}
        onChange={(e) => setMessage(e.target.value)}
        name='message'
      />
    </>
  )    
}

Incluya esto dentro de nuestra App con un interruptor para unirse al chatroom:

// src/App.tsx
import { useEffect, useState, useRef } from 'react'
import Chatroom from './Chatroom'
...
function App() {
  const [joinedChatroom, setJoinedChatroom] = useState(false)
  ...
  return (
    <>
      <h1>Hello RxStomp!</h1>

      <Status rxStomp={rxStomp}/>

      {!joinedChatroom && (
        <button onClick={() => setJoinedChatroom(true)}>
          Join chatroom!
        </button>
      )}

      {joinedChatroom && (
        <>
          <button onClick={() => setJoinedChatroom(false)}>
            Leave chatroom!
          </button>

          <Chatroom rxStomp={rxStomp}/>
        </>
      )}

    </>
  )

Listo para enviar mensajes reales. STOMP es ideal para enviar mensajes basados en texto (el envío de datos binarios también es posible). Definiremos la estructura de los datos que estamos enviando en un nuevo archivo types:

// types.ts
interface ChatMessage {
  userName: string,
  message: string
}

Nota: Si no utiliza TypeScript, puede omitir la adición de esta definición de tipo.

A continuación, vamos a usar JSON para serializar el mensaje y enviar mensajes a nuestro servidor STOMP utilizando .publish con un tema de destino y nuestro cuerpo JSON body.

// src/Chatroom.tsx
import type { ChatMessage } from './types'
...
const CHATROOM_NAME = '/topic/test'

export default function Chatroom(props: {rxStomp: RxStomp}) {
  ...
  function sendMessage(chatMessage: ChatMessage) {
    const body = JSON.stringify({ ...chatMessage })
    props.rxStomp.publish({ destination: CHATROOM_NAME, body })
    console.log(`Sent ${body}`)
    setMessage('')
  }

  return (
    <>
      <h2>Chatroom</h2>

      <label htmlFor="username">Username: </label>
      <input
        type="text"
        name="username"
        value={userName}
        onChange={(e) => setUserName(e.target.value)}
      />

      <label htmlFor="message">Message: </label>

      <input
        type="text"
        value={message}
        onChange={(e) => setMessage(e.target.value)}
        name="message"
      />

      <button onClick={() => sendMessage({userName, message})}>Send Message</button>
    </>
  )
}

Para probarlo, intente hacer clic en el botón Enviar Mensaje varias veces y verificar que la serialización funciona bien. Aunque no podrá ver cambios visuales todavía, los registros de la consola deberían mostrarlo:

Date ... >>> SEND
destination:/topic/test
content-length:45

Sent {"userName":"user722","message":"1234567890"}

Cómo Recibir Mensajes

Vamos a crear un nuevo componente para mostrar la lista de mensajes de todos los usuarios. Para ahora, usaremos el mismo tipo, pasaremos el nombre del tema como una propiedad, y mostraremos todo como una lista. Todo esto va a un nuevo componente llamado MessageList.

// src/MessageDisplay.tsx
import { useEffect, useState } from 'react'
import type { RxStomp } from '@stomp/rx-stomp'
import type { ChatMessage } from './types'

export default function MessageDisplay(props: {rxStomp: RxStomp, topic: string}) {
  const [chatMessages, setChatMessages] = useState<ChatMessage[]>([
    {userName: 'admin', message: `Welcome to ${props.topic} room!`}
  ])

  return(
  <>
  <h2>Chat Messages</h2>
  <ul>
    {chatMessages.map((chatMessage, index) => 
      <li key={index}>
        <strong>{chatMessage.userName}</strong>: {chatMessage.message}
      </li>
    )}
  </ul>
  </>
  )
}

¡Es hora de juntar todo!

Al igual que la gestión de la suscripción con el componente Status, configuramos la suscripción en el momento del montaje y desuscribimos al desmontar.

Usando RxJS pipe y map, podemos deserializar nuestro JSON de vuelta a nuestro ChatMessage. El diseño modular nos permite establecer una tubería más complicada según sea necesario usando operadores de RxJS.

// src/MessageDisplay.tsx
...
import { map } from 'rxjs'

export default function MessageDisplay(props: {rxStomp: RxStomp, topic: string}) {
  const [chatMessages, setChatMessages] = useState<ChatMessage[]>([
    {userName: 'admin', message: `Welcome to ${props.topic} room!`}
  ])

  useEffect(() => {
    const subscription = props.rxStomp
      .watch(props.topic)
      .pipe(map((message) => JSON.parse(message.body)))
      .subscribe((message) => setChatMessages((chatMessages) => [...chatMessages, message]))

    return () => {
      subscription.unsubscribe()
    }
  }, [])

  ...

En este punto, la interfaz de chat debería mostrar los mensajes correctamente, y puedes experimentar con abrir varias pestañas como diferentes usuarios.

Otra cosa a probar aquí es deshabilitar el servidor STOMP, enviar unos pocos mensajes, y volver a habilitarlo. Los mensajes deberían ser colocados en cola localmente y enviados una vez que el servidor esté listo para funcionar. ¡Guau!

Resumen

En este tutorial, hemos:

  • Instalado @stomp/rx-stomp para una experiencia de desarrollo agradable.

  • Configurado RxStompConfig para configurar nuestro cliente con los detalles de conexión, registro de depuración y ajustes de temporizador.

  • Usado rxStomp.activate y rxStomp.deactivate para gestionar el ciclo de vida principal del cliente.

  • Monitoreó el estado de suscripción usando el observable rxStomp.connectionState$.

  • Publicó mensajes usando rxStomp.publish con destinos y cuerpos de mensaje configurables.

  • Creó un observable para un tema dado usando rxStomp.watch.

  • Usó tanto registros en consola como componentes React para ver la biblioteca en acción y verificar la funcionalidad y tolerancia a fallos.

Puedes encontrar el código final en Gitlab: https://gitlab.com/harsh183/rxstomp-react-tutorial. Siéntete libre de usarlo como plantilla inicial también y reportar cualquier problema que pueda surgir.