Nest JS Websockets - Rooms
Tags: webdev, typescript, node, react
Published: 2022-12-08 16:44:10

Welcome to **Part 2: Rooms**, of building a realtime chat application with [Nest JS](https://nestjs.com/) and [React](https://reactjs.org/).

If you haven't checked out [Part 1: Basics](https://www.delightfulengineering.com/blog/nest-websockets/basics), go back to follow along as we've laid the foundations of the application there. We'll pick up where we left off there.

This series will attempt to touch on as many features of Nest's websockets integration as well as socket.io in general.

The entire series will be building on one repository that has both the server and the client for the application, so for each part you can download the repository from specific commits to follow along or you can clone the entire repository in it's finished state.

Download the repository at [this commit](https://github.com/ahoward2/nest-react-websockets/tree/17e2c2bf78dc54516021d80e8d31cd80c3548cd7) to follow along.

What we have now from Part 1 is a basic chat application where all connected clients are in the same global chat, in other words everyone connected to the server will receive everyone's messages.

So what we want to do now is take advantage of [rooms](https://socket.io/docs/v4/rooms/), is a key feature of **[socket.io](http://socket.io/)** which is our websockets framework of choice.

![Rooms Architecture](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9oj4wrcyj4vqmln0hv8z.png)

## Server Updates

We need to update our chat websocket gateway to subscribe to a new event that will allow users to join rooms.

Before we make any updates to our gateway, we need to create a new module to support user functionality.

To do this quickly, you can use the Nest CLI:

```bash
nest generate module user
nest generate service user
nest generate controller user
```

Now, the user module should look like this:

**server/user/user.module.ts**

```tsx
import { Module } from '@nestjs/common'
import { UserController } from './user.controller'
import { UserService } from './user.service'

@Module({
  controllers: [UserController],
  providers: [UserService],
  exports: [UserService],
})
export class UserModule {}
```

The two new important things we need to support rooms is our **user.service.ts** and **user.controller.ts** files.

### Shared Types

First let’s look at our shared types in **src/shared.**

**shared/interfaces/chat.interface.ts**

```tsx
export interface User {
  userId: string
  userName: string
  socketId: string
}

export interface Room {
  name: string
  host: User
  users: User[]
}

export interface Message {
  user: User
  timeSent: string
  message: string
  roomName: string
}

export interface ServerToClientEvents {
  chat: (e: Message) => void
}

export interface ClientToServerEvents {
  chat: (e: Message) => void
  join_room: (e: { user: User; roomName: string }) => void
}
```

The new type updates are that we introduce a new `socketId` type to the `User` interface as we’ll need to associate a unique socket to users.

We also have a new `Room` type where rooms will have a `name`, a `host` which will just be the user who created the room, and a list of `users` in the room.

We’re adding one more required type to our `Message` interface which is `roomName` since now all messages sent will be sent to a specific room (one room at a time for now.)

Finally we’re adding one more required type to our `ClientToServerEvents` interface as we’ll need clients to emit events to the server when they’re joining rooms.

### User Service

**server/user/user.service.ts**

```tsx
import { Injectable } from '@nestjs/common'
import { Room, User } from '../../shared/interfaces/chat.interface'

@Injectable()
export class UserService {
  private rooms: Room[] = []

  async addRoom(roomName: string, host: User): Promise<void> {
    const room = await this.getRoomByName(roomName)
    if (room === -1) {
      await this.rooms.push({ name: roomName, host, users: [host] })
    }
  }

  async removeRoom(roomName: string): Promise<void> {
    const findRoom = await this.getRoomByName(roomName)
    if (findRoom !== -1) {
      this.rooms = this.rooms.filter((room) => room.name !== roomName)
    }
  }

  async getRoomHost(hostName: string): Promise<User> {
    const roomIndex = await this.getRoomByName(hostName)
    return this.rooms[roomIndex].host
  }

  async getRoomByName(roomName: string): Promise<number> {
    const roomIndex = this.rooms.findIndex((room) => room?.name === roomName)
    return roomIndex
  }

  async addUserToRoom(roomName: string, user: User): Promise<void> {
    const roomIndex = await this.getRoomByName(roomName)
    if (roomIndex !== -1) {
      this.rooms[roomIndex].users.push(user)
      const host = await this.getRoomHost(roomName)
      if (host.userId === user.userId) {
        this.rooms[roomIndex].host.socketId = user.socketId
      }
    } else {
      await this.addRoom(roomName, user)
    }
  }

  async findRoomsByUserSocketId(socketId: string): Promise<Room[]> {
    const filteredRooms = this.rooms.filter((room) => {
      const found = room.users.find((user) => user.socketId === socketId)
      if (found) {
        return found
      }
    })
    return filteredRooms
  }

  async removeUserFromAllRooms(socketId: string): Promise<void> {
    const rooms = await this.findRoomsByUserSocketId(socketId)
    for (const room of rooms) {
      await this.removeUserFromRoom(socketId, room.name)
    }
  }

  async removeUserFromRoom(socketId: string, roomName: string): Promise<void> {
    const room = await this.getRoomByName(roomName)
    this.rooms[room].users = this.rooms[room].users.filter((user) => user.socketId !== socketId)
    if (this.rooms[room].users.length === 0) {
      await this.removeRoom(roomName)
    }
  }

  async getRooms(): Promise<Room[]> {
    return this.rooms
  }
}
```

A lot of these class methods are pretty straightforward and are in one way or another doing sorts of **getters**, **setters**, or directly mutating the `rooms` array which stores all active rooms on the server.

### User Controller

We need to create a controller for interacting with our user service via http as we’ll want to ping the server less often to retrieve all or specific rooms.

**server/user/user.controller.ts**

```tsx
import { Controller, Get, Param } from '@nestjs/common'
import { Room } from '../../shared/interfaces/chat.interface'
import { UserService } from './user.service'

@Controller()
export class UserController {
  constructor(private userService: UserService) {}

  @Get('api/rooms')
  async getAllRooms(): Promise<Room[]> {
    return await this.userService.getRooms()
  }

  @Get('api/rooms/:room')
  async getRoom(@Param() params): Promise<Room> {
    const rooms = await this.userService.getRooms()
    const room = await this.userService.getRoomByName(params.room)
    return rooms[room]
  }
}
```

Our server will now also have two GET routes to fetch active room data from which the clients can fetch.

- **api/rooms**: will retrieve all active rooms.
- **api/rooms/:room**: will retrieve one room via a `room` parameter.

Here are some sample JSONs from these routes to get a picture of the data structure.

**api/rooms response**

```json
[
  {
    "name": "secretroom",
    "host": {
      "socketId": "n6wiTBGp5Cja8F0qAAAB",
      "userId": "1,670,466,928,442Anna",
      "userName": "Anna"
    },
    "users": [
      {
        "socketId": "n6wiTBGp5Cja8F0qAAAB",
        "userId": "1,670,466,928,442Anna",
        "userName": "Anna"
      },
      {
        "socketId": "58r6srrtvBQP-o1pAAAD",
        "userId": "1,670,463,510,408Austin",
        "userName": "Austin"
      }
    ]
  },
  {
    "name": "meekosroom",
    "host": {
      "socketId": "5orXGez8W05N793CAAAF",
      "userId": "1,670,510,121,768Meeko",
      "userName": "Meeko"
    },
    "users": [
      {
        "socketId": "5orXGez8W05N793CAAAF",
        "userId": "1,670,510,121,768Meeko",
        "userName": "Meeko"
      }
    ]
  }
]
```

**api/rooms/secretroom response**

```json
{
  "name": "secretroom",
  "host": {
    "socketId": "n6wiTBGp5Cja8F0qAAAB",
    "userId": "1,670,466,928,442Anna",
    "userName": "Anna"
  },
  "users": [
    {
      "socketId": "n6wiTBGp5Cja8F0qAAAB",
      "userId": "1,670,466,928,442Anna",
      "userName": "Anna"
    },
    {
      "socketId": "58r6srrtvBQP-o1pAAAD",
      "userId": "1,670,463,510,408Austin",
      "userName": "Austin"
    }
  ]
}
```

### Chat Gateway

We’ll also be making a few updates to our existing chat gateway file so that we can support rooms.

**server/chat/chat.gateway.ts**

```tsx
import {
  MessageBody,
  SubscribeMessage,
  WebSocketGateway,
  WebSocketServer,
  OnGatewayConnection,
  OnGatewayDisconnect,
} from '@nestjs/websockets'
import { Logger } from '@nestjs/common'
import {
  ServerToClientEvents,
  ClientToServerEvents,
  Message,
  User,
} from '../../shared/interfaces/chat.interface'
import { Server, Socket } from 'socket.io'
import { UserService } from '../user/user.service'

@WebSocketGateway({
  cors: {
    origin: '*',
  },
})
export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect {
  constructor(private userService: UserService) {}

  @WebSocketServer() server: Server = new Server<ServerToClientEvents, ClientToServerEvents>()

  private logger = new Logger('ChatGateway')

  @SubscribeMessage('chat')
  async handleChatEvent(
    @MessageBody()
    payload: Message
  ): Promise<Message> {
    this.logger.log(payload)
    this.server.to(payload.roomName).emit('chat', payload) // broadcast messages
    return payload
  }

  @SubscribeMessage('join_room')
  async handleSetClientDataEvent(
    @MessageBody()
    payload: {
      roomName: string
      user: User
    }
  ) {
    if (payload.user.socketId) {
      this.logger.log(`${payload.user.socketId} is joining ${payload.roomName}`)
      await this.server.in(payload.user.socketId).socketsJoin(payload.roomName)
      await this.userService.addUserToRoom(payload.roomName, payload.user)
    }
  }

  async handleConnection(socket: Socket): Promise<void> {
    this.logger.log(`Socket connected: ${socket.id}`)
  }

  async handleDisconnect(socket: Socket): Promise<void> {
    await this.userService.removeUserFromAllRooms(socket.id)
    this.logger.log(`Socket disconnected: ${socket.id}`)
  }
}
```

There are some important new additions to our gateway.

Our `ChatGateway` class now implements `OnGatewayConnection`, and `OnGatewayDisconnect` which are two handy [lifecycle hooks](https://docs.nestjs.com/websockets/gateways#lifecycle-hooks) provided by Nest out of the box. With this we have two new functions to handle when there are socket connections or disconnections from clients to the server.

```tsx
// Will fire when a client connects to the server
async handleConnection(socket: Socket): Promise<void> {
    this.logger.log(`Socket connected: ${socket.id}`);
}

// Will fire when a client disconnects from the server
async handleDisconnect(socket: Socket): Promise<void> {
  await this.userService.removeUserFromAllRooms(socket.id);
  this.logger.log(`Socket disconnected: ${socket.id}`);
}
```

Subscribe to a `'join_room'` message which clients will send when they’re joining rooms. The **[socket.io](http://socket.io)** specific important function in here is the [`socketsJoin`](https://socket.io/docs/v4/server-instance/#socketsjoin) where will join a user’s `socketId` with the room they’re joining which is all sent through the payload.

```tsx
await this.server.in(payload.user.socketId).socketsJoin(payload.roomName)
```

Then we need to add the user to the internal state of our user service.

```tsx
await this.userService.addUserToRoom(payload.roomName, payload.user)
```

Note this is all being stored in memory on the server, if we were wanting to scale this up we may want to add an adapter for a dedicated data store like [redis](https://redis.io/) which we’ll do in later parts of this series.

## Client Updates

A couple of important new concepts we’re adding to our client:

- Client-side routing with [React Location](https://react-location.tanstack.com/).
- Client-server state management with [React Query](https://tanstack.com/query/v4/docs/overview).
- Joining / leaving rooms and viewing users in the room.

### App Entry Point and Routes

The application entry point (**index.tsx** and **app.tsx**) is now responsible for only core high level.

Our index.tsx hasn’t changed.

**client/index.tsx**

```tsx
import React from 'react'
import ReactDOM from 'react-dom'
import App from './app'
import './styles/main.css'

ReactDOM.render(<App />, document.getElementById('root'))
```

Our core app file previously contained the entirety of the user interface. Now since we’re introducing client-side routing and state management, we’ve trimmed down this file to contain a few things:

- [`queryClient`](https://tanstack.com/query/v4/docs/reference/QueryClient): used to interact with the React Query cache.
- [`QueryClientProvider`](https://tanstack.com/query/v4/docs/reference/QueryClientProvider): provider to connect and provide the query client to the application which uses React context under the hood.
- `AppRouter`: our application’s router which we’ve created to route between different pages.

**client/app.tsx**

```tsx
import React from 'react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { AppRouter } from './routes'

const queryClient = new QueryClient()

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <AppRouter />
    </QueryClientProvider>
  )
}

export default App
```

We’ve created our own file dedicated to routing. To use React Location we have to instantiate the `ReactLocation` class and pass `location` into the `Router` component that we’re returning from our `AppRouter`.

One particularly powerful feature of React Location is **Route Loaders**. For both of our routes we’re passing:

- `path`: url path to the route.
- `element`: react component (page) to render.
- `loader`: a function to fetch some data at the route level which is made available to the page we’ll see where we grab this loader data shortly when we get to our pages. This is great because now we can fetch data **as early as possible.**

The `<Outlet/>` component is where our routes will start rendering.

**client/routes.tsx**

```tsx
import React from 'react'
import { Outlet, ReactLocation, Router } from '@tanstack/react-location'
import Login, { loader as loginLoader } from './pages/login'
import Chat, { loader as chatLoader } from './pages/chat'

const location = new ReactLocation()

export const AppRouter = () => {
  return (
    <Router
      location={location}
      routes={[
        {
          path: '/',
          element: <Login />,
          loader: async () => await loginLoader(),
        },
        {
          path: 'chat',
          element: <Chat />,
          loader: async () => await chatLoader(),
        },
      ]}
    >
      <Outlet />
    </Router>
  )
}
```

### Layouts

Layouts are a great way to position content on pages at a high level.

Nothing particularly notable about these layouts other than that they take advantage of [composition](https://reactjs.org/docs/composition-vs-inheritance.html) (containment) by passing `children`.

**client/layouts/login.layout.tsx**

```tsx
import React from 'react'

export const LoginLayout = ({ children }: { children: React.ReactElement[] }) => {
  return (
    <div className="mx-auto flex h-screen w-screen justify-center bg-gray-900">
      <div className="h-127 w-127 my-auto flex flex-col p-2 md:flex-row">{children}</div>
    </div>
  )
}
```

Another thing to mention about layouts is that this is a good place to make your application responsive. You’ll see that with tailwind we’ve used size prefixes like `md:`, `lg:`, `xl:` to target different screen sizes.

**client/layouts/chat.layout.tsx**

```tsx
import React from 'react'

export const ChatLayout = ({ children }: { children: React.ReactElement[] }) => {
  return (
    <div className="mx-auto flex h-screen w-screen justify-center bg-gray-900">
      <div className="flex h-full w-full flex-col px-2 md:w-8/12 lg:w-6/12 xl:w-4/12">
        {children}
      </div>
    </div>
  )
}
```

### Pages

We’re splitting up our login and chat views into pages that can be routed to.

We’ll create a new **/pages** directory under **/client** and create two new pages.

**client/pages/login.tsx**

```tsx
import React, { useEffect, useState } from 'react'
import { MakeGenerics, useMatch, useNavigate } from '@tanstack/react-location'
import { User } from '../../shared/interfaces/chat.interface'
import { LoginForm } from '../components/login.form'
import { Rooms } from '../components/rooms'
import { LoginLayout } from '../layouts/login.layout'
import { useRoomsQuery } from '../lib/room'
import { generateUserId, getUser, setUser } from '../lib/user'

function Login() {
  const {
    data: { user, roomName },
  } = useMatch<LoginLocationGenerics>()

  const [joinRoomSelection, setJoinRoomSelection] = useState<string>('')

  const { data: rooms, isLoading: roomsLoading } = useRoomsQuery()

  const navigate = useNavigate()

  const login = (e: React.FormEvent<HTMLFormElement>) => {
    const userFormValue = e.target[0].value
    const roomFormValue = e.target[1].value
    const newUser = {
      userId: generateUserId(userFormValue),
      userName: userFormValue,
    }
    setUser({ id: newUser.userId, name: newUser.userName })
    if (joinRoomSelection !== '') {
      sessionStorage.setItem('room', joinRoomSelection)
    } else {
      sessionStorage.setItem('room', roomFormValue)
    }
    navigate({ to: '/chat' })
  }

  useEffect(() => {
    if (user?.userId && roomName) {
      navigate({ to: '/chat', replace: true })
    }
  }, [])

  return (
    <LoginLayout>
      <Rooms
        rooms={rooms ?? []}
        selectionHandler={setJoinRoomSelection}
        selectedRoom={joinRoomSelection}
        isLoading={roomsLoading}
      ></Rooms>
      <LoginForm
        defaultUser={user?.userName}
        disableNewRoom={joinRoomSelection !== ''}
        login={login}
      ></LoginForm>
    </LoginLayout>
  )
}

export const loader = async () => {
  const user = getUser()
  return {
    user: user,
    roomName: sessionStorage.getItem('room'),
  }
}

type LoginLocationGenerics = MakeGenerics<{
  LoaderData: {
    user: Pick<User, 'userId' | 'userName'>
    roomName: string
  }
}>

export default Login
```

Let’s drill in piece by piece in the Login page and pick apart what’s going on.

First lets look at the bottom of the file at `loader` and `LoginLocationGenerics`.

We’re exporting this loader function for use in our **routes.tsx** file to load some data that we want ahead of page rendering time. Both the `user` and `roomName` return values are being grabbed from sessionStorage.

```tsx
export const loader = async () => {
  const user = getUser()
  return {
    user: user,
    roomName: sessionStorage.getItem('room'),
  }
}
```

`getUser()` is just a helper method for getting user values from session storage.

**client/lib/user.ts**

```tsx
//...

export const getUser = () => {
  const userId = sessionStorage.getItem('userId')
  const userName = sessionStorage.getItem('userName')
  return {
    userId,
    userName,
  }
}

//...
```

With this loader data we can define a generic type with `MakeGenerics` from React Location which gives us some type safety for the loader data which we’ll extract in our page - we’ll see that next.

```tsx
type LoginLocationGenerics = MakeGenerics<{
  LoaderData: {
    user: Pick<User, 'userId' | 'userName'>
    roomName: string
  }
}>
```

We can use `useMatch` from React Location to get our loader data - and pass our generic type to it.

```tsx
const {
  data: { user, roomName },
} = useMatch<LoginLocationGenerics>()
```

Our login page has both a form to create and join a new room, and also to select from existing rooms that have been created. To store an existing room selection we make use of a simple `useState` hook from React.

```tsx
const [joinRoomSelection, setJoinRoomSelection] = useState<string>('')
```

To fetch the existing rooms we’ve created a custom `useRoomsQuery` hook.

```tsx
const { data: rooms, isLoading: roomsLoading } = useRoomsQuery()
```

Under the hood for this we’re using `useQuery` from React Query and fetching our server’s `/api/rooms` endpoint with axios. This effectively loads all data for all rooms into the client query cache - freaking awesome!

**client/lib/rooms.ts**

```tsx
//...

export const useRoomsQuery = () => {
  const query = useQuery({
    queryKey: ['select_rooms'],
    queryFn: (): Promise<Room[]> => axios.get(`/api/rooms`).then((response) => response.data),
  })
  return query
}

//...
```

There are a few scenarios for the Login page for which we may want or need the user to navigate to another route.

Whenever a user creates or joins an existing room, or if a user attempts to come to the login page while already being logged in and in an existing room - we need to direct them over to the Chat page.

To do this sort of navigation, we can use `useNavigate` from React Location - so here we instantiate it.

```tsx
const navigate = useNavigate()
```

We need a function to handle when a user submits the login form, so we’ve created a `login` function to handle just that. We’re grabbing form values and also checking if the user has chosen an existing room to join - and finally navigation to the `/chat` route.

```tsx
const login = (e: React.FormEvent<HTMLFormElement>) => {
  const userFormValue = e.target[0].value
  const roomFormValue = e.target[1].value
  const newUser = {
    userId: generateUserId(userFormValue),
    userName: userFormValue,
  }
  setUser({ id: newUser.userId, name: newUser.userName })
  if (joinRoomSelection !== '') {
    sessionStorage.setItem('room', joinRoomSelection)
  } else {
    sessionStorage.setItem('room', roomFormValue)
  }
  navigate({ to: '/chat' })
}
```

The second scenario for if a user is already logged in and in an existing room, but tries to navigate to the Login page, we need to direct them back to the Chat page.

We can do this with a `useEffect` that runs only once on the first render to do some validation and perform the navigate.

```tsx
useEffect(() => {
  if (user?.userId && roomName) {
    navigate({ to: '/chat', replace: true })
  }
}, [])
```

Finally we return the UI components.

```tsx
return (
  <LoginLayout>
    <Rooms
      rooms={rooms ?? []}
      selectionHandler={setJoinRoomSelection}
      selectedRoom={joinRoomSelection}
      isLoading={roomsLoading}
    ></Rooms>
    <LoginForm
      defaultUser={user?.userName}
      disableNewRoom={joinRoomSelection !== ''}
      login={login}
    ></LoginForm>
  </LoginLayout>
)
```

Let’s see what our `Rooms` UI component looks like.

This `Rooms` component is relatively straightforward, but one thing of note is a delay functionality we’ve added. When rooms data gets loaded, there’s a potential for that data to load fast - causing the loading spinner to flicker. To avoid this we’re intentionally delaying the rendering of the rooms and keeping the spinner on the page for a minimum of `1000` milliseconds.

**client/components/rooms.tsx**

```tsx
import React, { useEffect, useState } from 'react'
import { Room } from '../../shared/interfaces/chat.interface'
import { Loading } from './loading'

export const Rooms = ({
  rooms,
  selectionHandler,
  selectedRoom,
  isLoading,
}: {
  rooms: Room[]
  selectionHandler: (roomName: string) => void
  selectedRoom?: string
  isLoading: boolean
}) => {
  const [isDelay, setIsDelay] = useState(true)
  useEffect(() => {
    const delayTimer = setTimeout(() => {
      setIsDelay(false)
    }, 1000)
    return () => {
      clearTimeout(delayTimer)
    }
  }, [])

  return (
    <div className="h-full w-full rounded-lg border border-slate-400 bg-gray-800 md:h-1/2">
      <div className="flex justify-between rounded-t-md border border-slate-400 bg-slate-400 p-2">
        <span>Join existing rooms</span>
        {selectedRoom && <button onClick={() => selectionHandler('')}>Clear</button>}
      </div>
      <div className="w-full">
        {!isLoading &&
          !isDelay &&
          rooms.map((room, index) => (
            <button
              key={index}
              className={
                selectedRoom === room.name
                  ? 'w-full bg-slate-900 p-2 text-left text-gray-400'
                  : ' w-full p-2 text-left text-gray-400'
              }
              onClick={() => selectionHandler(room.name)}
            >
              {room.name}
            </button>
          ))}
        {(isLoading || isDelay) && <Loading />}
      </div>
    </div>
  )
}
```

The `LoginForm` is almost as straightforward as it gets!

**client/components/login.form.tsx**

```tsx
import React from 'react'

export const LoginForm = ({
  login,
  disableNewRoom,
  defaultUser,
}: {
  login: (e: React.FormEvent<HTMLFormElement>) => void
  disableNewRoom: boolean
  defaultUser?: string
}) => {
  return (
    <div className="h-full w-full py-2 md:px-2 md:py-0">
      <form
        onSubmit={(e) => {
          e.preventDefault()
          login(e)
        }}
        className="flex flex-col justify-center"
      >
        <input
          type="text"
          id="login"
          placeholder="Name"
          defaultValue={defaultUser && defaultUser}
          required={true}
          className="mb-2 h-12 rounded-md border border-slate-400 bg-gray-800 text-white placeholder-slate-400 focus:border-violet-500 focus:outline-none focus:ring-1 focus:ring-violet-500"
        ></input>
        <input
          type="text"
          id="room"
          disabled={disableNewRoom}
          placeholder="New room"
          className="mb-2 h-12 rounded-md border border-slate-400 bg-gray-800 text-white placeholder-slate-400 focus:border-violet-500 focus:outline-none focus:ring-1 focus:ring-violet-500 disabled:opacity-50"
        ></input>
        <button
          type="submit"
          className="flex h-12 w-full items-center justify-center rounded-md bg-violet-700 text-white"
        >
          Join
        </button>
      </form>
    </div>
  )
}
```

Here are some snapshots of the Login page!


![Login UI one](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0c8hjgftq3wkk2c0ie22.png)


![Login UI two](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xdcm6e5uu3i652sa0dl1.png)


![Login UI three](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mn5gy8s0kfudowvbos4v.png)

Let’s move on to the really fun part of the client - the Chat page!

**client/pages/chat.tsx**

```tsx
import React, { useState, useEffect } from 'react'
import { MakeGenerics, useMatch, useNavigate } from '@tanstack/react-location'
import { io, Socket } from 'socket.io-client'
import {
  User,
  Message,
  ServerToClientEvents,
  ClientToServerEvents,
} from '../../shared/interfaces/chat.interface'
import { Header } from '../components/header'
import { UserList } from '../components/list'
import { MessageForm } from '../components/message.form'
import { Messages } from '../components/messages'
import { ChatLayout } from '../layouts/chat.layout'
import { unsetRoom, useRoomQuery } from '../lib/room'
import { getUser } from '../lib/user'

const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io({
  autoConnect: false,
})

function Chat() {
  const {
    data: { user, roomName },
  } = useMatch<ChatLocationGenerics>()

  const [isConnected, setIsConnected] = useState(socket.connected)
  const [messages, setMessages] = useState<Message[]>([])
  const [toggleUserList, setToggleUserList] = useState<boolean>(false)

  const { data: room } = useRoomQuery(roomName, isConnected)

  const navigate = useNavigate()

  useEffect(() => {
    if (!user || !roomName) {
      navigate({ to: '/', replace: true })
    } else {
      socket.on('connect', () => {
        socket.emit('join_room', {
          roomName,
          user: { socketId: socket.id, ...user },
        })
        setIsConnected(true)
      })

      socket.on('disconnect', () => {
        setIsConnected(false)
      })

      socket.on('chat', (e) => {
        setMessages((messages) => [e, ...messages])
      })

      socket.connect()
    }
    return () => {
      socket.off('connect')
      socket.off('disconnect')
      socket.off('chat')
    }
  }, [])

  const leaveRoom = () => {
    socket.disconnect()
    unsetRoom()
    navigate({ to: '/', replace: true })
  }

  const sendMessage = (message: string) => {
    if (user && socket && roomName) {
      socket.emit('chat', {
        user: {
          userId: user.userId,
          userName: user.userName,
          socketId: socket.id,
        },
        timeSent: new Date(Date.now()).toLocaleString('en-US'),
        message,
        roomName: roomName,
      })
    }
  }
  return (
    <>
      {user?.userId && roomName && room && (
        <ChatLayout>
          <Header
            isConnected={isConnected}
            users={room?.users ?? []}
            roomName={roomName}
            handleUsersClick={() => setToggleUserList((toggleUserList) => !toggleUserList)}
            handleLeaveRoom={() => leaveRoom()}
          ></Header>
          {toggleUserList ? (
            <UserList room={room}></UserList>
          ) : (
            <Messages user={user} messages={messages}></Messages>
          )}
          <MessageForm sendMessage={sendMessage}></MessageForm>
        </ChatLayout>
      )}
    </>
  )
}

export const loader = async () => {
  const user = getUser()
  return {
    user: user,
    roomName: sessionStorage.getItem('room'),
  }
}

type ChatLocationGenerics = MakeGenerics<{
  LoaderData: {
    user: Pick<User, 'userId' | 'userName'>
    roomName: string
  }
}>

export default Chat
```

Okay, the Chat page has quite a bit going on - but a couple of things like the `loader`, `MakeGenerics`, and the `useMatch` to load route data are all the same as the Login page so we’ll go over what’s new.

Since the last part of the series we’ve updated one part of instantiating our `socket` which is passing `autoConnect: false`. By disabling automatic connection - we are taking control over the when the connection happens. Why did we need this? Well there’s one scenario where if a user attempts to navigate to the Chat page while not being logged in or in a specific room, if automatic connection is true then a socket connection would occur when we don’t want it to. We first need to do some validation before connecting the socket.

```tsx
const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io({
  autoConnect: false,
})
```

We have some of the same `useState` hooks from the last series but also added one additional to store a flag for whether to display current users connected to the room.

```tsx
const [isConnected, setIsConnected] = useState(socket.connected)
const [messages, setMessages] = useState<Message[]>([])
const [toggleUserList, setToggleUserList] = useState<boolean>(false)
```

We need data stored on the server about the room, so we had a custom `useRoomQuery` hook to fetch data from the server.

```tsx
const { data: room } = useRoomQuery(roomName, isConnected)
```

The `useRoomsQuery` hook fetches data from axios and we use a couple of options to customize it’s behavior.

Since we’re primarily using this to fetch all users that are connected to the room, we want it to poll the server on an interval since people may be leaving or joining at any given time - in other words that data will go stale.

To handle polling, we pass `refetchInterval: 60000` which will refetch data every 60 seconds.

Additionally we only want this query to run if the socket is connected so we pass `enabled: isConnected` as well.

**client/lib/room.ts**

```tsx
export const useRoomQuery = (roomName, isConnected) => {
  const query = useQuery({
    queryKey: ['rooms', roomName],
    queryFn: (): Promise<Room> =>
      axios.get(`/api/rooms/${roomName}`).then((response) => response.data),
    refetchInterval: 60000,
    enabled: isConnected,
  })
  return query
}
```

Similar to the Login page, we have two scenarios where we want the user to navigate off of the page, so we need to instantiate `useNavigate` from React Location.

```tsx
const navigate = useNavigate()
```

There are two important new things that we added to the `useEffect` on the Chat page.

- Checking if `user` and `roomName` exists - and if either does not exist then we want to navigate back to the Login page route.
- When the socket `'connect'` event occurs, we want to emit a `'join_room'` event to the server which will add us to the new or existing room.

```tsx
useEffect(() => {
  if (!user || !roomName) {
    navigate({ to: '/', replace: true })
  } else {
    socket.on('connect', () => {
      socket.emit('join_room', {
        roomName,
        user: { socketId: socket.id, ...user },
      })
      setIsConnected(true)
    })

    socket.on('disconnect', () => {
      setIsConnected(false)
    })

    socket.on('chat', (e) => {
      setMessages((messages) => [e, ...messages])
    })

    socket.connect()
  }
  return () => {
    socket.off('connect')
    socket.off('disconnect')
    socket.off('chat')
  }
}, [])
```

We also added the ability for users to leave rooms.

```tsx
const leaveRoom = () => {
  socket.disconnect()
  unsetRoom()
  navigate({ to: '/', replace: true })
}
```

The `unsetRoom` function is pretty simple. It just removes the current room from session storage.

**client/lib/room.ts**

```tsx
export const unsetRoom = () => {
  sessionStorage.removeItem('room')
}
```

The `sendMessage` also hasn’t changed much and just handles sending messages to the room. We did add `roomName` as another part of the chat message data so the server knows which room to send the message in.

```tsx
const sendMessage = (message: string) => {
  if (user && socket && roomName) {
    socket.emit('chat', {
      user: {
        userId: user.userId,
        userName: user.userName,
        socketId: socket.id,
      },
      timeSent: new Date(Date.now()).toLocaleString('en-US'),
      message,
      roomName: roomName,
    })
  }
}
```

Finally we return UI components.

```tsx
return (
  <>
    {user?.userId && roomName && room && (
      <ChatLayout>
        <Header
          isConnected={isConnected}
          users={room?.users ?? []}
          roomName={roomName}
          handleUsersClick={() => setToggleUserList((toggleUserList) => !toggleUserList)}
          handleLeaveRoom={() => leaveRoom()}
        ></Header>
        {toggleUserList ? (
          <UserList room={room}></UserList>
        ) : (
          <Messages user={user} messages={messages}></Messages>
        )}
        <MessageForm sendMessage={sendMessage}></MessageForm>
      </ChatLayout>
    )}
  </>
)
```

The `Header` component is very straightforward. We have the following features in the `Header`:

- A 🟢 or 🔴 circle indicating the connected state of the user’s socket.
- The name of the room the user is connected to.
- A button 👨‍💻 that will toggle the display between messages and connected users in the room.
- A **Leave** button which directs users back to the Login page.

![Header UI component](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1pkykmruwxp85etpapv3.png)

**client/components/header.tsx**

```tsx
import React from 'react'
import { User } from '../../shared/interfaces/chat.interface'

export const Header = ({
  isConnected,
  users,
  handleUsersClick,
  handleLeaveRoom,
  roomName,
}: {
  isConnected: boolean
  users: User[]
  handleUsersClick: () => void
  handleLeaveRoom: () => void
  roomName: string
}) => {
  return (
    <header className="flex h-1/6 flex-col pt-12">
      <div className="flex justify-between">
        <div className="flex h-8 items-center">
          <span className="ml-1">{isConnected ? '🟢' : '🔴'}</span>
          <span className="px-2 text-3xl text-white">{'/'}</span>
          <span className=" text-white">{roomName}</span>
        </div>
        <div className="flex">
          <button
            onClick={() => handleUsersClick()}
            className="ml-1 flex h-8 items-center rounded-xl bg-gray-800 px-4"
          >
            <span className="mr-1 text-lg text-white">{'👨‍💻'}</span>
            <span className="ml-1 text-white">{users.length}</span>
          </button>
          <button
            onClick={() => handleLeaveRoom()}
            className="ml-1 flex h-8 items-center rounded-xl bg-gray-800 px-4"
          >
            <span className="mr-1 text-white">{'Leave'}</span>
          </button>
        </div>
      </div>
    </header>
  )
}
```

The `Messages` component displays all messages in the room since the user has been connected. There is no persistent storage yet but we may be adding this in later parts of this series.

We have the following features in the `Messages` component:

- Display messages with the latest message at the bottom.
- Allow scrolling on the y-axis.
- Display the user and the time sent.
- Style the current user’s messages in a light gray and a left margin, and the opposite for all other users.

![Messages UI component](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/011s1p2g3niindjtxt34.png)

**client/components/messages.tsx**

```tsx
import React from 'react'
import { Message, User } from '../../shared/interfaces/chat.interface'

const determineMessageStyle = (user: Pick<User, 'userId' | 'userName'>, messageUserId: string) => {
  if (user && messageUserId === user.userId) {
    return {
      message: 'bg-slate-500 p-4 ml-24 mb-4 rounded break-words',
      sender: 'ml-24 pl-4',
    }
  } else {
    return {
      message: 'bg-slate-800 p-4 mr-24 mb-4 rounded break-words',
      sender: 'mr-24 pl-4',
    }
  }
}

export const Messages = ({
  user,
  messages,
}: {
  user: Pick<User, 'userId' | 'userName'>
  messages: Message[]
}) => {
  return (
    <div className="flex h-4/6 w-full flex-col-reverse overflow-y-scroll">
      {messages?.map((message, index) => {
        return (
          <div key={index}>
            <div className={determineMessageStyle(user, message.user.userId).sender}>
              <span className="text-sm text-gray-400">{message.user.userName}</span>
              <span className="text-sm text-gray-400">{' ' + '•' + ' '}</span>
              <span className="text-sm text-gray-400">{message.timeSent}</span>
            </div>
            <div className={determineMessageStyle(user, message.user.userId).message}>
              <p className="text-white">{message.message}</p>
            </div>
          </div>
        )
      })}
    </div>
  )
}
```

The `UserList` component has the following features:

- Display all current users in the room.
- Display a 👑 next to the host of the room.

![UserList UI component](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iuidwp7kvjotvekqrz6t.png)

**client/components/list.tsx**

```tsx
import React from 'react'
import { Room } from '../../shared/interfaces/chat.interface'

export const UserList = ({ room }: { room: Room }) => {
  return (
    <div className="flex h-4/6 w-full flex-col-reverse overflow-y-scroll">
      {room.users.map((user, index) => {
        return (
          <div key={index} className="mb-4 flex rounded px-4 py-2">
            <p className="text-white">{user.userName}</p>
            {room.host.userId === user.userId && <span className="ml-2">{'👑'}</span>}
          </div>
        )
      })}
    </div>
  )
}
```

Lastly, and importantly we have the `MessageForm` component where users can input new messages.

The `MessageForm` component has the following features:

- Allow users to submit a message via either a `onKeyDown` event when the user presses the `'Enter'` key or an `onClick` event on the send button.
- Once a message is sent, clear the form value.

![Message form UI component](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6w6nozgjfbxkpkxlc67o.png)

**client/components/message.form.tsx**

```tsx
import React, { useRef } from 'react'

export const MessageForm = ({ sendMessage }: { sendMessage: (message: string) => void }) => {
  const textAreaRef = useRef<HTMLTextAreaElement>(null)

  const submit = (e) => {
    e.preventDefault()
    const value = textAreaRef?.current?.value
    if (value) {
      sendMessage(value)
      textAreaRef.current.value = ''
    }
  }

  const handleKeyDown = (e) => {
    if (e.key === 'Enter') {
      submit(e)
    }
  }

  return (
    <div className="flex h-1/6 items-center">
      <form className="flex w-full appearance-none rounded-md bg-gray-800 outline-none focus:outline-none">
        <textarea
          ref={textAreaRef}
          onKeyDown={(e) => handleKeyDown(e)}
          id="minput"
          placeholder="Message"
          className="mb-2 max-h-16 flex-grow appearance-none rounded-md border-none bg-gray-800 text-white placeholder-slate-400 focus:outline-none focus:ring-transparent"
        ></textarea>
        <button onClick={(e) => submit(e)} className="self-end p-2 text-slate-400">
          <svg
            xmlns="http://www.w3.org/2000/svg"
            fill="none"
            viewBox="0 0 24 24"
            strokeWidth={1.5}
            stroke="currentColor"
            className="h-4 w-4 bg-gray-800"
          >
            <path
              strokeLinecap="round"
              strokeLinejoin="round"
              d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5"
            />
          </svg>
        </button>
      </form>
    </div>
  )
}
```

Here are the snapshots of the Chat page now!

![Chat page UI one](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m78a1yqi1jti580fucc9.png)

![Chat page UI two](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hnqpux7mwn80dnqfe6ne.png)

That's all for this part of the series.