← Back to blog
INTEGRATION6 min read

Add Realtime to a React App with NoLag

HB
Henco Burger
July 28, 2026

React apps built with Vite, Create React App, or any other bundler can add realtime with a small custom hook. The hook owns the connection lifecycle so your components just render messages and call a publish function. This works in any React setup; if you are on Next.js, see the Next.js guide for the extra server-token step.

1. Install the SDK

npm install @nolag/js-sdk

2. A reusable hook

import { useEffect, useRef, useState, useCallback } from 'react'
import { NoLag } from '@nolag/js-sdk'
import type { RoomContext } from '@nolag/js-sdk'

// KRAKEN_URL is your broker, for example ws://localhost:8080/ws when kraken
// runs locally. Read it from your bundler's environment variables.

type Message = { text: string }

export function useNoLagRoom(token: string, app: string, roomName: string, topic: string) {
  const [messages, setMessages] = useState([] as Message[])
  const roomRef = useRef(null as RoomContext | null)

  useEffect(() => {
    const client = NoLag(token, { url: KRAKEN_URL })
    const room = client.setApp(app).setRoom(roomName)
    room.on(topic, (data) => setMessages((m) => [...m, data as Message]))
    // Subscribe on every connect: the handler runs after each reconnect too,
    // and a self-hosted broker does not reliably restore subscriptions
    client.on('connect', () => {
      room.subscribe(topic)
      roomRef.current = room
    })
    client.connect().catch((err) => console.error('NoLag connect failed', err))

    return () => {
      roomRef.current = null
      client.disconnect()
    }
  }, [token, app, roomName, topic])

  const publish = useCallback(
    (data: Message) => {
      roomRef.current?.emit(topic, data)
      // Publishers never receive their own messages, so append it locally
      setMessages((m) => [...m, data])
    },
    [topic],
  )

  return { messages, publish }
}

Subscribing happens in the connect handler rather than once after connect(), because the handler runs again after every successful reconnect. The publish function appends the message to local state as well as emitting it. NoLag never delivers a message back to the actor that published it, so without that line the sender's own text would never show up in their list.

3. Use it in a component

// The app slug and room your auth config grants this token (kraken's
// auth.json or a @nolag/core project document). Rooms are never created
// by subscribing.
const APP_SLUG = 'chat'

function Chat({ token }: { token: string }) {
  const { messages, publish } = useNoLagRoom(token, APP_SLUG, 'general', 'message')

  return (
    <div>
      <ul>
        {messages.map((m, i) => (
          <li key={i}>{m.text}</li>
        ))}
      </ul>
      <button onClick={() => publish({ text: 'Hello!' })}>Send</button>
    </div>
  )
}

Keep the token off the client

Fetch the token from your own backend rather than hardcoding a long-lived access token in the bundle. With @nolag/core, the production pattern for browser sessions is a short-lived client token your backend signs with a project signing key, scoped to what that user may do.

Why the cleanup matters

The hook's cleanup function runs on unmount and when the dependencies change. Clearing the room ref and disconnecting prevents duplicate subscriptions and leaked WebSocket connections as users navigate around your app.

Next steps