Full-Stack React With Phoenix (Chapter 9 | Channels)

Table of Contents

Chapter 1 | Why Bother?
Chapter 2 | Learning the Basics of Elixir
Chapter 3 | Introduction to Phoenix
Chapter 4 | Implementing React
Chapter 5 | Working With PostgreSQL
Chapter 6 | Creating a PostgreSQL API Service
Chapter 7 | CRUD Operations
Chapter 8 | User Authentication

What Are Channels?

High-Level Understanding

Channels provide bi-directional communication via messages.

Think of it like having walkie talkies.

By being on the same channel, two people could communicate messages with one another from both directions. In the same way, Phoenix channels enable communication to occur between the backend and the frontend (server and client).

This enables “real-time” features for applications built with React and Phoenix.

Some use cases of real-time features include instant messaging, document collaboration (multiple people editing a document at same time), analytics, and binary streaming.

In this chapter, we will be making a real-time chat application.

As mentioned, channels are based upon the sending and receiving of messages between the server and client. Because of this, channels include senders and receivers.

Just like in normal conversations, there is always a topic of conversation. Likewise, senders broadcast messages about certain topics. Receivers subscribe to topics to receive the messages that were broadcasted by the senders. Messages passing through a channel can have multiple receivers.

Diving Deeper

So far, we’ve described channels at a high-level. Let’s unpack the many layers in more specifics.

Sockets
A physical socket is a cylinder with two open ends:

In the same way, sockets in a technical sense are the tunnels for messages being passed on two open ends (the server and client).

Socket Handlers
As the official documentation summarizes nicely, Socket handlers are modules that authenticate and identify a socket connection and allow you to set default socket assigns for use in all channels.

Topics
Topics are string identifiers of what messages are about. By knowing what messages are about, we can make sure the messages end up in the right place when being handled.

The string identifiers are written about with the following syntax: topic:subtopic

An example would be foods:chicken.

Messages
Messages are structs that contain a topic, event, payload, and ref.

Topics were just discussed.

Events are the string for the event name.

Payloads contain the data to be communicated within a message.

Refs are unique string identifiers for a message.

Channel Routes
We mentioned senders and receivers which broadcast messages and subscribe to messages on certain topics accordingly. Channel routes are used to route topics with channel modules.

For example, let’s say we wanted to have a channel about foods. We could have something like this:

channel "foods:*", PhoenixApp.FoodsChannel

The code above would route all discussion about foods into a channel module in a Phoenix app called FoodsChannel.

The * is a wildcard matcher for subtopics so requests for foods:chicken and foods:apples would both go into the FoodsChannel.

Channel Modules
Channel Modules (also just called channels) are basically bi-directional controllers. They contain events for incoming and outgoing stuff.

Transports
As the name suggests, the transport layer handles the transportation of messages into and out of a channel.

PubSub
The PubSub (publisher/subscriber) layer consists of using a predefined Phoenix.PubSub module. This module contains functions that handle certain tasks for organizing channel communication such as: subscribing to topics, unsubscribing from topics, and broadcasting messages on a topic.

Creating a Phoenix/React Chat Room

Configuring Our First Channel Topic

Let’s generate a new Phoenix project called chat_app:

mix phoenix.new chat_app
cd chat_app

Open the project in a code editor of choice.

In lib/chat_app/endpoint.ex, there is an endpoint for communication via a socket already defined:

defmodule ChatApp.Endpoint do
use Phoenix.Endpoint, otp_app: :chat_app
  socket "/socket", ChatApp.UserSocket
# ....

In web/channels/user_socket.ex, we have a socket for communication of messages with a transportation layer configured:

transport :websocket, Phoenix.Transports.WebSocket

In this file, we can uncomment the following line:

## Channels
channel "room:*", ChatApp.RoomChannel # uncommented

The code above is saying: “Handle all subtopics of the room topic in our channel module called RoomChannel.”

room refers to having a single chat room that can have all sorts of subtopics.

Next, we need to write the code for our the channel module called RoomChannel which handles all the messages with a topic of room.

Create another file called room_channel.ex under web/channels.

In this file, we can add the following:

defmodule ChatApp.RoomChannel do
use Phoenix.Channel
  def join("room:lobby", _message, socket) do
{:ok, socket}
end

end

In the code above, we are allowing anyone to join room:lobby topic and communication through the socket.

Client Configuration

Now, the client is ready to join the room:lobby topic in our channel to send messages. Let’s setup our React frontend to do this.

You should know the basic setup a React frontend but let’s do it together.

First, we install all dependencies:

npm install --save react react-dom babel-preset-react

Then, open brunch-config.js to apply the Babel preprocessing for our React code:

plugins: {
babel: {
presets: ["es2015", "react"],
// Do not use ES6 compiler in vendor code
ignore: [/web\/static\/vendor/]
}
},

In addition, we need to add a whitelist in under the npm options in this file so that it is clear that we are going to use react and react-dom:

npm: {
enabled: true,
whitelist: ["phoenix", "phoenix_html", "react", "react-dom"]
}

We can create the target for our app in the index.html.eex template (delete everything else):

<div id="app"></div>

The index.html.eex template is nested within app.html.eex. The app template contains a pre-populated header which we can remove:

<header class="header">
<nav role="navigation">
<ul class="nav nav-pills pull-right">
<li><a href="http://www.phoenixframework.org/docs">Get Started</a></li>
</ul>
</nav>
<span class="logo"></span>
</header>
^^^ delete this

Under web/static/js, let’s add containers and presentationals folders.

In web/static/js/app.js, we can write the top-level React component for our app which will render a container component called Chat:

import "phoenix_html";
import React from "react";
import ReactDOM from "react-dom";
import Chat from "./containers/Chat";
class App extends React.Component {
render() {
return (
<Chat />
)
}
}
ReactDOM.render(
<App/>,
document.getElementById("app")
)

Let’s create that Chat component in a file called Chat.js under the containers folder.

We can populate the shell of the code for this component:

import React from "react";
class Chat extends React.Component {
render() {
return (
<div>
</div>
)
}
}
export default Chat;

After that, we want to connect to our socket and attempt to join the channel for our lobby:

import React from "react";
import {Socket} from "phoenix"
class Chat extends React.Component {
componentDidMount() {
let socket = new Socket("/socket", {params: {token: window.userToken}})
    socket.connect()
    let channel = socket.channel("room:lobby", {})
    channel.join()
.receive("ok", response => { console.log("Joined successfully", response) })
}
  render() {
return (
<div>
</div>
)
}
}
export default Chat

Notice that we are doing the following:

  • Set socket to the endpoint as configured in lib/chat_app/endpoint.ex
  • Connect socket
  • Store our channel in variable also called channel
  • Join the channel log a successful response

If we are able to join successfully, then we should have “Joined successfully” logged in our console.

Let’s run mix phoenix.server and check this out on http://localhost:4000/:

Woot woot!

Rendering User Messages

Let’s add an input component and have our types messages render to the UI.

I’m once again going to be using Bulma. To configure Bulma, add the following snippet in the head of web/templates/layout/app.html.eex:

<link rel="stylesheet" href="
https://cdnjs.cloudflare.com/ajax/libs/bulma/0.5.0/css/bulma.css">

Back in our Chat component, let’s render an input and button element within a form:

render() {
return (
<div>
<form onSubmit={this.handleSubmit.bind(this)} >
<div className="field">
<label
className="label"
style={{
textAlign: "left"
}}
>
GenericUsername:
</label>
<div className="control">
<input
className="input"
type="text"
style={{
marginTop: "10px"
}}
/>
</div>
</div>
<button
type="submit"
value="Submit"
className="button is-primary"
style={{
marginTop: "10px"
}}
>
Submit
</button>
</form>
</div>
)
}

Let’s add a local state property called inputMessage that stores the current value of the input. We also need an event handler called handleInputMessage to update inputMessage on every change:

class Chat extends React.Component {
constructor() {
super();
this.state = {
inputMessage: ""
}
}
 //componentDidMount stuff
  handleInputMessage(event) {
this.setState({
inputMessage: event.target.value
})
}
  render() {
return (
<div>
<form onSubmit={this.handleSubmit.bind(this)} >
<div className="field">
<label
className="label"
style={{
textAlign: "left"
}}
>
GenericUsername:
</label>
<div className="control">
<input
className="input"
type="text"
style={{
marginTop: "10px"
}}
value = {this.state.inputMessage}
onChange = {this.handleInputMessage.bind(this)}
/>
</div>
</div>
<button
type="submit"
value="Submit"
className="button is-primary"
style={{
marginTop: "10px"
}}
>
Submit
</button>
</form>
</div>
)
}
export default Chat

Notice that we also have onSubmit={this.handleSubmit.bind(this)} to handle a form submission. Let’s add that event handler:

handleSubmit(event) {
event.preventDefault();
console.log(this.state.inputMessage);
}

For now, it will just log the current input message so we can confirm everything is working so far.

Run brunch build and let’s check the local host:

Sweet! Everything looks great at this point.

Next, we want the handleSubmit function to push submitted input messages to an array in our local state which we can call userMessages:

constructor() {
super();
this.state = {
inputMessage: "",
userMessages: []
}
}
handleSubmit(event) {
event.preventDefault();
this.setState({
userMessages: this.state.userMessages.concat(this.state.inputMessage)
})
}

Next, let’s render a presentational component for each message in userMessages:

render() {
const userMessages = this.state.userMessages.map((message, index) =>
<UserMessage
key = { index }
username = { "GenericUsername" }
message = { message }
/>
);
return (
<div>
//form here
{userMessages}
</div>
)
}

We need to then make the UserMessage component. Create a file called UserMessage.js under the presentationals folder and let’s add the following:

import React from "react";
class UserMessage extends React.Component {
  render() {
return (
<div className="box" style={{ marginBottom: "10px" }}>
<article className="media">
<div className="media-content">
<div className="content">
<p>
<strong>{this.props.username}</strong>
<br/>
{this.props.message}
</p>
</div>
</div>
</article>
</div>
)
}
}
export default UserMessage

Back in Chat, let’s import this component:

import UserMessage from '../presentationals/UserMessage';

I’m also going to wrap the injected UserMessage components within a flexbox container:

<div
className="flex-container"
style={{
display: "flex",
flexDirection: "column",
alignItems: "flexStart",
justifyContent: "flexStart",
margin: "auto",
width: "100%"
}}
>
{userMessages}
</div>

Run brunch build and let’s check the local host:

We can now see our messages rendering.

Really quickly, let’s improve the experience by emptying inputMessage on every submission:

handleSubmit(event) {
event.preventDefault();
this.setState({
userMessages: this.state.userMessages.concat(this.state.inputMessage),
inputMessage: ""
})
}

Sending and Receiving Messages

Currently, we are just rendering the values of the input to our UI. We aren’t pushing them out as messages via a channel.

First, let’s go to Chat.js and push inputMessage to the channel:

handleSubmit(event) {
event.preventDefault();
channel.push("new_msg", {body: this.state.inputMessage})
this.setState({
userMessages: this.state.userMessages.concat(this.state.inputMessage),
inputMessage: ""
})
}

channel.push(“new_msg”, {body: this.state.inputMessage}) consists of two main parts.

“new_msg” the string defining the event name.

{body: this.state.inputMessage} is the payload which contains the data.

We will now have to write the server-side code to handle this incoming event.

Open web/channels/room_channel.ex and add the following:

defmodule ChatApp.RoomChannel do
use Phoenix.Channel
  def join("room:lobby", _message, socket) do
{:ok, socket}
end
  def handle_in("new_msg", %{"body" => body}, socket) do
broadcast socket, "new_msg", %{body: body}
{:noreply, socket}
end
end

We have added a function called handle_in which takes the payload (body) of the incoming message via the socket and broadcasts that message back.

Let’s finish up by adding the logic for our React frontend to receive the message broadcasted from the server and render it to the UI.

Open Chat.js and let’s add another array in our local state to keep track of these messages from the server:

constructor() {
super();
this.state = {
inputMessage: "",
userMessages: [],
serverMessages: []
}
}

In componentWillMount(), let’s have the payload of the incoming “new_msg” message logged to the console:

componentWillMount() {
let socket = new Socket("/socket", {params: {token: window.userToken}})
socket.connect()
  let channel = socket.channel("room:lobby", {})
channel.join()
.receive("ok", response => { console.log("Joined successfully", response) })
  channel.on("new_msg", payload => {
console.log(payload);
})
}

If you run brunch build, you will see the following error:

The issue is that we are doing channel.push(..) in handleSubmit but channel is within the scope of componentWillMount(). To resolve this, let’s adjust the code in componentWillMount():

class Chat extends React.Component {
constructor() {
super();
this.state = {
inputMessage: "",
userMessages: [],
serverMessages: [],
}
    let socket = new Socket("/socket", {params:
{token: window.userToken}
});
socket.connect();
    this.channel = socket.channel("room:lobby", {});
}
  componentWillMount() {
this.channel.join()
.receive("ok", response => { console.log("Joined successfully", response) })
    this.channel.on("new_msg", payload => {
this.setState({
serverMessages: this.state.serverMessages.concat(payload.body)
})
})
}

We have moved the channel configuration to the constructor and stored the channel in this.channel.

Now, we can just do this.channel whenever we want to reference our channel in our class. I already did this for componentWillMount() but let’s also do it for handleSumbit:

handleSubmit(event) {
event.preventDefault();
this.channel.push("new_msg", {body: this.state.inputMessage})
this.setState({
userMessages: this.state.userMessages.concat(this.state.inputMessage),
inputMessage: ""
})
}

Now, we can run brunch build with no issues. Try sending a message and check the console:

As expected, we have the payload is an object with a key called body and a value of “Can you hear me?”.

Awesome!

Let’s push the body value from the payload to the serverMessages array:

this.channel.on("new_msg", payload => {
this.setState({
serverMessages: this.state.serverMessages.concat(payload.body)
})
})

Now, let’s render a component called ServerMessage for each message in the serverMessages array:

render() {
const userMessages = this.state.userMessages.map((message, index) =>
<UserMessage
key = { index }
username = { "GenericUsername" }
message = { message }
/>
);
  const serverMessages = this.state.serverMessages.map((message, index) =>
<ServerMessage
key = { index }
username = { "Server" }
message = { message }
/>
);

Then, we inject this like so:

{userMessages}
{serverMessages}

Let’s create the ServerMessage component in a file called ServerMessage.js in the presentationals folder and the following:

import React from "react";
class ServerMessage extends React.Component {
  render() {
return (
<div className="box" style={{ marginBottom: "10px" }}>
<article className="media">
<div className="media-content">
<div className="content">
<p>
<strong>{this.props.username}</strong>
<br/>
{this.props.message}
</p>
</div>
</div>
</article>
</div>
)
}
}
export default ServerMessage

Just like UserMessage, we are taking the passed down props to present the message within a box defined by Bulma.

Lastly, let’s import this component in Chat.js:

import ServerMessage from '../presentationals/ServerMessage';

Run brunch build and test this out:

Chat With Phoenix Test

Cool beans!

There’s just one issue. Multiple user messages skew the correct order of the rendered boxes in our UI:

This is because of the fact that we are injecting the user messages and server messages separately:

{userMessages}
{serverMessages}

To correct this, we need to just nest one single collection of messages.

First, let’s change our local state to just have one array called messages:

this.state = {
inputMessage: "",
messages: []
}

Delete userMessages and serverMessages above the return in our render and use find and replace to update every use of userMessages and serverMessages to just messages.

Then, we can map through messages and render the right component based on the index:

const messages = this.state.messages.map((message, index) => {
if(index % 2 == 0) {
return (
<UserMessage
key = { index }
username = { "GenericUsername" }
message = { message }
/>
)
} else {
return (
<ServerMessage
key = { index }
username = { "Server" }
message = { message }
/>
)
}
});

Lastly, we can inject messages:

{messages}

Now, our messages are alternating in the correct order:

That’s all she wrote!

💪 🔥 💚 🙌 ✋

Final Code

Available on GitHub.

Concluding Thoughts

After doing writing tutorials on all sorts of technology for the past year, I’ve realized that it’s best to teach fundamentals in simple, small projects and leave it up to you to do the more advanced examples.

With that being said, I think this chat app was quite simple but also quite helpful for learning how to work with channels in Phoenix. For a challenge, try to see how you can creatively use real-time messaging in your own project. A fun idea would to make a simple chat bot where the server responds to specific phrases differently.

With channels covered, there is one final chapter for this book where we will wrap things up.

Final Chapter

The final chapter is now available.

Sign Up for Notifications

Get notified when each chapter is released.


Cheers,
Mike Mangialardi
Founder of Coding Artist