pwshub.com

How to Use RxStomp with React – Build Chat App with STOMP and React

How to Use RxStomp with React – Build Chat App with STOMP and React

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 browsers that do not allow 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

  • Prerequisites

  • Starter STOMP Server with RabbitMQ

  • Starter React Template

  • How to Install RxStomp

  • How to Manage Connection and Disconnection with the STOMP Server

  • How to Monitor the Connection Status

  • How to Send Messages

  • How to Receive Messages

  • Summary

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.

Source: freecodecamp.org

Related stories
5 hours ago - When you’re building a website, it’s important to make sure that it’s fast. People have little to no patience for slow-loading websites. So as developers, we need to use all the techniques available to us to speed up our site’s...
1 month ago - Have you ever used an attribute in HTML without fully understanding its purpose? You're not alone! Over time, I've dug into the meaning behind many HTML attributes, especially those that are crucial for accessibility. In this in-depth...
1 month ago - This tutorial teaches you how to use the where() function to select elements from your NumPy arrays based on a condition. You'll learn how to perform various operations on those elements and even replace them with elements from a separate...
1 month ago - Data surrounds us, but its raw form can be overwhelming and difficult to interpret. That's where data visualization comes in. It can help you take your data and turn it into charts and graphs that make sense at a glance. Among the many...
1 month ago - Inside ChatGPT, when you start a conversation, you can choose the available model like “GPT-4o”, “o1-mini”, etc., and under all of these, there’s an option for Temporary chat. When toggled on, your chat with ChatGPT will become, well, a...
Other stories
12 minutes ago - Warehouse management software allows businesses to optimize their supply chain process, improve warehouse efficiency, and manage order fulfillment. With the rise of eCommerce merchants, global markets, and rising customer expectations,...
3 hours ago - One of the best things about the Raspberry Pi 5 (other than the performance boost over its predecessor) is how much easier it is to add an SSD. And using an SSD with the Raspberry Pi 5 is a no-brainer if you’re running a proper desktop OS...
5 hours ago - In any software project, documentation plays a crucial role in guiding developers, users, and stakeholders through the project's features and functionalities. As projects grow and evolve, managing documentation across various...
6 hours ago - I've got a few pages here that are primarily built for my own use. One of them, my bots page, is a list of all the dumbsuper useful bots I've built for Mastodon (and Bluesky). The idea on this page is to show the latest post from each...
7 hours ago - Message brokers play a very important role in distributed systems and microservices. Developers should know if RabbitMQ or Kafka fits best.