Let’s Build |> a Slack Clone with Elixir, Phoenix, and React (part 5 — Phoenix Sockets and Channels)

Live Demo — GitHub Repo
Part 1 — Part 2 — Part 3 — Part 4 — Part 5 — Part 6 — Part 7

In part 4 we built a way for users to create chat rooms. What we have so far is a pretty basic web app, but here is where things get fun. We will be creating a Phoenix channel so we can write messages that will be broadcast in real time to all users who are present in the chat room.

Before we go any further, we will need to create a messages database table.

mix phoenix.gen.model Message messages room_id:references:rooms user_id:references:users text:string
sling/api/priv/repo/migrations/timestamp_create_message.exs

Run the migration

mix ecto.migrate

Update the model

sling/api/web/models/message.ex

Add has_many :messages, Sling.Message to the schema in both user.ex and room.ex to link the relationship.

Creating Sockets

This is usually the point where we create a route and a controller, but data that we broadcasted to multiple users will be loaded through a channel instead. To connect to a channel, first we need to users to connect to a socket. We begin by updating the user_socket.ex file that Phoenix already generated for us.

sling/api/web/channels/user_socket.ex

We specify that channel requests that start with “rooms:*” will go to our yet to be created RoomChannel.

The connect/1 function takes a token as a parameter, so to connect to the channel we will need to send the jwt from the frontend. We can use Guardian to validate the token, so only logged in users can connect to the channel.

Our GuardianSerializer is then used to load the current user from the token, and then we assign the current_user to the socket. This will let us access the value in socket.assigns.current_user from inside a channel.

Creating a Channel

Now let’s create a simple implementation of the RoomChannel

The join method needs a room_id, so we can have different channels for each chat room. We can then load the room from our repo, and use our existing RoomView to build a json object containing the room’s data.

The last step {:ok, response, assign(socket, :room, room)} sends the reply to the frontend with an ok status, the json response, and assigns the current room to the socket, which will let us access the room in socket.assigns.room later in this channel when we create new messages.

Frontend Socket Connections

Before we begin creating messages, let’s try connecting to the channel from the frontend. To connect to any Phoenix channel, we need to first connect to the user_socket, and all we need for that is a user id. It would make sense to connect right after a user logs in, and then store the socket in the redux store, so it can be accessed in multiple containers. So let’s update our session actions:

sling/web/src/actions/session.js

If you check out /sling/api/lib/sling/endpoint.ex, you’ll see Phoenix already defined the socket url with the line socket “/socket”, Sling.UserSocket, which we’ll stick with for our socket connection. While http requests are made to http://…, websocket connections are made to ws://…, so I’ve defined a WEBSOCKET_URL that replaces either https or http from our API_URL and strips the /api ending, so we just have to request the /socket endpoint.

The new connectToSocket function will take our jwt from localStorage and use the Phoenix Socket javascript library to make a request to the user_socket.ex connect/1 function. Finally we dispatch a SOCKET_CONNECTED action type, so let’s catch that in the session reducer.

sling/web/src/reducers/session.js

Frontend Channel Connections

Cool, now we should be able to access that socket in the Room container, and connect to the RoomChannel. Here is the function we will need to connect to the room channel, let’s create it in a new room.js action file.

sling/web/src/action/room.js

And we will want to track the current room’s state in a new reducer, room.js:

sling/web/src/reducers/room.js

The new room reducer then needs to be added to our root reducer:

sling/web/src/reducers/index.js

Now, inside the Room container, we can connect to the channel in componentDidMount, and leave the channel when the id in params changes, or when the component unmounts.

sling/web/src/containers/Room/index.js

That should be it for connecting to the socket and room channel. Go ahead and log in, enter and exit a room and watch your console to see the Socket logger show you the API responses.

Let’s take a quick break to check out the current git diff

Creating Messages in Channels

Let’s start on the backend again, and add a way to retrieve and create messages in the RoomChannel. Looking ahead to the functionality we need, we will want a way to paginate messages by scrolling up in the list.

scrivener_ecto is a cool library that will help us out with pagination. To install, add {:scrivener_ecto, “~> 1.0”} to your dependencies in mix.exs and add :scrivener_ecto to the application list. We also need to tell our Repo to use Scrivener in repo.ex

sling/api/lib/sling/repo.ex

Run mix deps.get to fetch the library, and restart your Phoenix server.

Now we can use Scrivener in our Ecto query to load messages, and include pagination information in the response.

sling/api/web/channels/room_channel.ex

|> Sling.Repo.paginate() is a function supplied by Scrivener that actually executes the query. It takes a page and a page_size parameter, but we will just be using the default 25 page_size we defined in the configuration. And instead of requesting certain page numbers when we paginate, we will need to request messages that are older than a certain message id, because as messages are created and broadcasted, the total_entries and pagination info we may have previously retrieved will no longer be accurate. But the pagination library will help us see if there are older messages present and if we should be able to paginate.

We also need to create a MessageView to render the message to json: (Note that we had to |> preload(:user) in the query above to be able to include the message’s user data)

sling/api/web/views/message_view.ex

Next we need to create the PaginationHelpers that we are using in the RoomChannel response:

sling/api/web/views/pagination_helpers.ex

In the RoomChannel above, we created a handle_in/1 function that will handle new_message actions that are pushed to the channel from the frontend. It looks similar to a controller action, but we use our private method broadcast_message to broadcast! the message’s json response to all user’s who are connected to the channel. So we just need to listen for “message_created” in the channel on the frontend to update a message list with redux.

Frontend Channel Actions

Let’s jump over to React, and create the those actions:

sling/web/src/actions/room.js

The channel.on(‘message_created’… will listen for new message for all users connected to a channel, and dispatch MESSAGE_CREATED to update that user’s redux store with the new message.

channel.push(‘new_message’, data) is how we communicate a new message has been created in a channel. I am wrapping that in a Promise because it helps with redux-form, allowing us to receive a submitting prop, and also reset the form when the Promise resolves.

Let’s update our room reducer to listen for MESSAGE_CREATED events:

sling/web/src/reducers/room.js

When a message is created, it will simply be added to the end of the array of current messages.

Our query returns the newest 25 messages, with the first item being the newest. In our chat room we want to show the newest message on bottom. Since this is more of a UI concern that a backend responsibility, I’m calling reverse() on those messages to put them in the correct order.

Now we can begin implementing our UI to show a list of messages and a form to create new messages. We can start by adding these components to our Room container

The first new component is the RoomNavbar

sling/web/src/components/RoomNavbar/index.js

Then we have to create our MessageList. Most of the logic here is just structuring the messages into groups based on the day they were created.

sling/web/src/components/MessageList/index.js

We also map over the messages, rendering a new Message component:

sling/web/src/components/Message/index.js

And each Message shows an Avatar for each user, fetching a gravatar image:

sling/web/src/components/Avatar/index.js

Now we just need the MessageForm component, so we can write new messages:

sling/web/src/components/MessageForm/index.js

Now type some messages into the new form and watch them appear in the MessageList. Then, open up a separate incognito browser window and login as a separate user and write a new message. You should see the message appear instantly for both logged in users.

This blog section is getting pretty long, but at this point we have a fully functioning chat room.

Here is the last git commit for part 5

In part 6, we will be adding the Phoenix Presence module to show a list of all the active users in a chat room. We’ll also make some UI improvements to the MessageList, and add scrolling pagination.

Read part 6 or view the live demo