wasp/web/docs/advanced/web-sockets.md
Martin Šošić 594e8446b4
Updated docs to 0.12. (#1783)
======================

Updated docs/advanced/apis to 0.12. (#1729)

Updated docs/advanced/middleware-config to 0.12. (#1730)

Updated docs/advanced/links to 0.12. (#1732)

Updated docs/advanced/web-sockets to 0.12. (#1733)

Updated docs/advanced/email to 0.12. (#1734)

Auth docs rework (#1723)

Updated docs/advanced/jobs to 0.12. (#1740)

Updates starter templates (#1744)

Updates general docs (#1745)

Update customising app docs (#1748)

Updates client setup (#1749)

Change server config docs (#1750)

Updates CSS frameworks docs (#1753)

Updates static assets docs (#1751)

Updates custom vite config docs (#1754)

Update testing docs (#1752)

Updated docs/introduction/* to 0.12. (#1756)

Restructuring data model docs (#1739)

Updates depedencies docs (#1768)

Co-authored-by: Mihovil Ilakovac <mihovil@ilakovac.com>
2024-02-19 13:35:32 +01:00

9.8 KiB

title
Web Sockets

import useBaseUrl from '@docusaurus/useBaseUrl'; import { ShowForTs } from '@site/src/components/TsJsHelpers'; import { Required } from '@site/src/components/Tag';

Wasp provides a fully integrated WebSocket experience by utilizing Socket.IO on the client and server.

We handle making sure your URLs are correctly setup, CORS is enabled, and provide a useful useSocket and useSocketListener abstractions for use in React components.

To get started, you need to:

  1. Define your WebSocket logic on the server.
  2. Enable WebSockets in your Wasp file, and connect it with your server logic.
  3. Use WebSockets on the client, in React, via useSocket and useSocketListener.
  4. Optionally, type the WebSocket events and payloads for full-stack type safety.

Let's go through setting up WebSockets step by step, starting with enabling WebSockets in your Wasp file.

Turn On WebSockets in Your Wasp File

We specify that we are using WebSockets by adding webSocket to our app and providing the required fn. You can optionally change the auto-connect behavior.

app todoApp {
  // ...

  webSocket: {
    fn: import { webSocketFn } from "@src/webSocket",
    autoConnect: true, // optional, default: true
  },
}
app todoApp {
  // ...

  webSocket: {
    fn: import { webSocketFn } from "@src/webSocket",
    autoConnect: true, // optional, default: true
  },
}

Defining the Events Handler

Let's define the WebSockets server with all of the events and handler functions.

:::info Full-stack type safety Check this out: we'll define the event types and payloads on the server, and they will be automatically exposed on the client. This helps you avoid mistakes when emitting events or handling them. :::

webSocketFn Function

On the server, you will get Socket.IO io: Server argument and context for your WebSocket function. The context object give you access to all of the entities from your Wasp app.

You can use this io object to register callbacks for all the regular Socket.IO events. Also, if a user is logged in, you will have a socket.data.user on the server.

This is how we can define our webSocketFn function:

import { v4 as uuidv4 } from 'uuid'
import { getFirstProviderUserId } from 'wasp/auth'

export const webSocketFn = (io, context) => {
  io.on('connection', (socket) => {
    const username = getFirstProviderUserId(socket.data.user) ?? 'Unknown'
    console.log('a user connected: ', username)

    socket.on('chatMessage', async (msg) => {
      console.log('message: ', msg)
      io.emit('chatMessage', { id: uuidv4(), username, text: msg })
      // You can also use your entities here:
      // await context.entities.SomeEntity.create({ someField: msg })
    })
  })
}
import { v4 as uuidv4 } from 'uuid'
import { getFirstProviderUserId } from 'wasp/auth'
import { type WebSocketDefinition, type WaspSocketData } from 'wasp/server/webSocket'

export const webSocketFn: WebSocketFn = (io, context) => {
  io.on('connection', (socket) => {
    const username = getFirstProviderUserId(socket.data.user) ?? 'Unknown'
    console.log('a user connected: ', username)

    socket.on('chatMessage', async (msg) => {
      console.log('message: ', msg)
      io.emit('chatMessage', { id: uuidv4(), username, text: msg })
      // You can also use your entities here:
      // await context.entities.SomeEntity.create({ someField: msg })
    })
  })
}

// Typing our WebSocket function with the events and payloads
// allows us to get type safety on the client as well

type WebSocketFn = WebSocketDefinition<
  ClientToServerEvents,
  ServerToClientEvents,
  InterServerEvents,
  SocketData
>

interface ServerToClientEvents {
  chatMessage: (msg: { id: string, username: string, text: string }) => void;
}

interface ClientToServerEvents {
  chatMessage: (msg: string) => void;
}

interface InterServerEvents {}

// Data that is attached to the socket.
// NOTE: Wasp automatically injects the JWT into the connection,
// and if present/valid, the server adds a user to the socket.
interface SocketData extends WaspSocketData {}

Using the WebSocket On The Client

:::info Full-stack type safety All the hooks we use are typed with the events and payloads you defined on the server. VS Code will give you autocomplete for the events and payloads, and you will get type errors if you make a mistake. :::

useSocket Hook

Client access to WebSockets is provided by the useSocket hook. It returns:

  • socket: Socket for sending and receiving events.
  • isConnected: boolean for showing a display of the Socket.IO connection status.
    • Note: Wasp automatically connects and establishes a WebSocket connection from the client to the server by default, so you do not need to explicitly socket.connect() or socket.disconnect().
    • If you set autoConnect: false in your Wasp file, then you should call these as needed.

All components using useSocket share the same underlying socket.

useSocketListener Hook

Additionally, there is a useSocketListener: (event, callback) => void hook which is used for registering event handlers. It takes care of unregistering the handler on unmount.

import React, { useState } from 'react'
import {
  useSocket,
  useSocketListener,
} from 'wasp/client/webSocket'

export const ChatPage = () => {
  const [messageText, setMessageText] = useState('')
  const [messages, setMessages] = useState([])
  const { socket, isConnected } = useSocket()

  useSocketListener('chatMessage', logMessage)

  function logMessage(msg) {
    setMessages((priorMessages) => [msg, ...priorMessages])
  }

  function handleSubmit(e) {
    e.preventDefault()
    socket.emit('chatMessage', messageText)
    setMessageText('')
  }

  const messageList = messages.map((msg) => (
    <li key={msg.id}>
      <em>{msg.username}</em>: {msg.text}
    </li>
  ))
  const connectionIcon = isConnected ? '🟢' : '🔴'

  return (
    <>
      <h2>Chat {connectionIcon}</h2>
      <div>
        <form onSubmit={handleSubmit}>
          <div>
            <div>
              <input
                type="text"
                value={messageText}
                onChange={(e) => setMessageText(e.target.value)}
              />
            </div>
            <div>
              <button type="submit">Submit</button>
            </div>
          </div>
        </form>
        <ul>{messageList}</ul>
      </div>
    </>
  )
}

Wasp's full-stack type safety kicks in here: all the event types and payloads are automatically inferred from the server and are available on the client 🔥

You can additionally use the ClientToServerPayload and ServerToClientPayload helper types to get the payload type for a specific event.

import React, { useState } from 'react'
import {
  useSocket,
  useSocketListener,
  ServerToClientPayload,
} from 'wasp/client/webSocket'

export const ChatPage = () => {
  const [messageText, setMessageText] = useState<
    // We are using a helper type to get the payload type for the "chatMessage" event.
    ClientToServerPayload<'chatMessage'>
  >('')
  const [messages, setMessages] = useState<
    ServerToClientPayload<'chatMessage'>[]
  >([])
  // The "socket" instance is typed with the types you defined on the server.
  const { socket, isConnected } = useSocket()

  // This is a type-safe event handler: "chatMessage" event and its payload type
  // are defined on the server.
  useSocketListener('chatMessage', logMessage)

  function logMessage(msg: ServerToClientPayload<'chatMessage'>) {
    setMessages((priorMessages) => [msg, ...priorMessages])
  }

  function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault()
    // This is a type-safe event emitter: "chatMessage" event and its payload type
    // are defined on the server.
    socket.emit('chatMessage', messageText)
    setMessageText('')
  }

  const messageList = messages.map((msg) => (
    <li key={msg.id}>
      <em>{msg.username}</em>: {msg.text}
    </li>
  ))
  const connectionIcon = isConnected ? '🟢' : '🔴'

  return (
    <>
      <h2>Chat {connectionIcon}</h2>
      <div>
        <form onSubmit={handleSubmit}>
          <div>
            <div>
              <input
                type="text"
                value={messageText}
                onChange={(e) => setMessageText(e.target.value)}
              />
            </div>
            <div>
              <button type="submit">Submit</button>
            </div>
          </div>
        </form>
        <ul>{messageList}</ul>
      </div>
    </>
  )
}

API Reference

app todoApp {
  // ...

  webSocket: {
    fn: import { webSocketFn } from "@src/webSocket",
    autoConnect: true, // optional, default: true
  },
}
app todoApp {
  // ...

  webSocket: {
    fn: import { webSocketFn } from "@src/webSocket",
    autoConnect: true, // optional, default: true
  },
}

The webSocket dict has the following fields:

  • fn: WebSocketFn

    The function that defines the WebSocket events and handlers.

  • autoConnect: bool

    Whether to automatically connect to the WebSocket server. Default: true.