How to Use RxStomp with React: Build A Chat App

Harsh DeepHarsh Deep
9 min read

STOMP is an amazingly simple yet powerful protocol for sending messages implemented by popular servers like RabbitMQ, ActiveMQ, and Apollo. Using STOMP over WebSocket is a straightforward protocol, making it a popular choice for sending messages from a web browser because protocols like AMQP are limited by major browsers blocking TCP connections.

To use STOMP over WebSocket, you can use @stomp/stompjs, but that has tricky callbacks and a complicated API that caters to more specialized use cases. Luckily, there’s also the lesser-known @stompjs/rx-stomp which provides a nice interface via RxJS observables. Observables aren't exclusive to Angular, and they fit quite well with how React works. It's a neat interface when composing complex workflows and pipelines with many different message sources.

The tutorial follows a somewhat similar path as the initial version in Angular, but the component structure and code style are tuned towards the functional style of React.

Note: This tutorial is written with strict TypeScript, but the JavaScript code is almost identical since we only have 5 type declarations. For the JS version, you can skip the type imports and definitions.

Table of Contents

Goals

Here, we’ll build a simplified chatroom application that shows various aspects of RxStomp across different components. Overall, we want to have:

  • A React frontend connected with RxStomp to a STOMP server.

  • A live connection status display based on our connection to the STOMP server.

  • Pub/Sub logic for any configurable topic.

  • Splitting RxStomp logic across multiple components to show how to separate logic and responsibility.

  • Aligning RxStomp connection/subscription lifecycles with React component lifecycles to ensure that there are no leaks or unclosed watchers.

Prerequisites

  • You should have a STOMP server running so that the React application can connect to it. Here, we’ll use RabbitMQ with the rabbitmq_web_stomp extension.

  • Latest React version. This tutorial will use v18, although older versions will probably work as well.

  • Some familiarity with observables will also help.

Starter STOMP Server with RabbitMQ

If you’d like to use RabbitMQ too (not strictly required), here’s are installation guides for different operating systems. To add the extension, you’ll need to run:

$ rabbitmq-plugins enable rabbitmq_web_stomp

If you’re able to use Docker, a Docker file similar to this will set everything needed for the tutorial:

FROM rabbitmq:3.8.8-alpine

run rabbitmq-plugins enable --offline rabbitmq_web_stomp

EXPOSE 15674

Starter React Template

For this tutorial, we'll use Vite's react-ts template. The central part of our application will be in the App component, and we'll create child components for other specific STOMP functionality.

How to Install RxStomp

We’ll use the @stomp/rx-stomp npm package:

$ npm i @stomp/rx-stomp rxjs

This will install version 2.0.0

Note: This tutorial still works without explicitly specifying rxjs since it's a sister dependency, but it's good practice to be explicit about it.

How to Manage Connection and Disconnection with the STOMP Server

Now, let's open App.tsx and initialize our RxStomp client. Since the client isn't a state that will change for rendering, we’ll wrap it in the useRef Hook.

// 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

Assuming the default ports and authentication details, we’ll define some configuration for our connection next.

// 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() {
  ...

For a better dev experience, we logged all messages with timestamps to a local console and set low timer frequencies. Your configuration should be quite different for your production application, so check out the RxStompConfig docs for all the options available.

Next, we’ll pass the configuration to rxStomp inside a useEffect Hook. This manages the connection's activation alongside the component lifecycle.

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

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

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

While there's no visual change in our app, checking the logs should show connection and ping logs. Here's an example of what that should look like:

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

Note: Generally, if you see duplicate logs, it may be a sign that a deactivation or unsubscribe functionality wasn't implemented correctly. React renders each component twice in a dev environment to help people catch these bugs via React.StrictMode

How to Monitor the Connection Status

RxStomp has a RxStompState enum that represents possible connection states with our broker. Our next goal is to display the connection status in our UI.

Let's create a new component for this called Status.tsx:

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

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

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

We can use the rxStomp.connectionState$ observable to bind to our connectionStatus string. Similar to how we used useEffect, we’ll use the unmount action to 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>
    </>
  )
}

To view it, we include it in our app:

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

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

At this point, you should have a working visual indicator on the screen. Try playing around by taking the STOMP server down and see if the logs work as expected.

How to Send Messages

Let's create a simple chatroom to show a simplified end-to-end messaging flow with the broker.

We can place the functionality in a new Chatroom component. First, we can create the component with a custom username and message field that's bound to inputs.

// 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'
      />
    </>
  )    
}

Let’s include this within our App with a toggle to join the 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}/>
        </>
      )}

    </>
  )

Time to actually send messages. STOMP is best for sending text-based messages (binary data is also possible). We’ll define the structure of the data we're sending in a new types file:

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

Note: If you're not using TypeScript, you can skip adding this type definition.

Next, let's use JSON to serialize the message and send messages to our STOMP server using .publish with a destination topic and our 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>
    </>
  )
}

To test it out, try clicking the Send Message button a few times and see if the serialization works fine. While you won't be able to see any visual changes yet, the console logs should show it:

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

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

How to Receive Messages

We’ll create a new component to show the list of messages from all the users. For now, we'll use the same type, pass the topic name as a prop, and display everything as a list. All this goes into a new component called 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>
  </>
  )
}

Time to bring everything together!

Similar to managing the subscription with the Status component, we set up the subscription on mount, and unsubscribe on unmount.

Using RxJS pipe and map, we can deserialize our JSON back to our ChatMessage. The modular design can let you set up a more complicated pipeline as needed using RxJS operators.

// 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()
    }
  }, [])

  ...

At this point, the chat GUI should show messages correctly, and you can experiment with opening multiple tabs as different users.

Another thing to try here is turning off the STOMP server, sending a few messages, and turning it back on. The messages should get queued locally and dispatched once the server is ready to go. Neat!

Summary

In this tutorial, we:

  • Installed @stomp/rx-stomp for a nice dev experience.

  • Set up RxStompConfig to configure our client with the connection details, debugger logging and timer settings.

  • Used rxStomp.activate and rxStomp.deactivate to manage the client’s main lifecycle.

  • Monitored the subscription state using rxStomp.connectionState$ observable.

  • Published messages using rxStomp.publish with configurable destinations and message bodies.

  • Created an observable for a given topic using rxStomp.watch.

  • Used both console logs and React components to see the library in action, and verify functionality and fault tolerance.

You can find the final code on Gitlab: https://gitlab.com/harsh183/rxstomp-react-tutorial. Feel free to use it as a starter template too and report any issues that may come up.

0
Subscribe to my newsletter

Read articles from Harsh Deep directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Harsh Deep
Harsh Deep