Phoenix & React Channels

I finally found a nice excuse for messing around with Phoenix channels. I decided to start saving the deckstring whenever a user exports a deck in hearthdecks, this will be a pretty straighforward implementation. For an actual tutorial I recommend the official Phoenix docs on Channels, they are very well done.

Server Side

First thing we do is create a new socket module that uses Phoenix.Socket, phoenix automatically generates a UserSocket file that I edited to match below. We want to make sure the channel that we're defining is pointing to our new channel module that we'll add next. Important to note that the first argument "room" is known as the topic, if you have multiple channels you can do use a wildcard like: "room:*" here, and then in your channel module do something like "room:" <> room_id to ensure you are matching correctly.

defmodule HearthdecksWeb.DeckSocket do
  use Phoenix.Socket

  ## Channels
  channel("room", HearthdecksWeb.DeckChannel)

  ## Transports
  transport(:websocket, Phoenix.Transports.WebSocket)

  def connect(_params, socket) do
    {:ok, socket}
  end

  def id(_socket), do: nil
end

Next we'll create our new DeckChannel module, this will be what is handling any messages sent from the client. The first argument "room" in our join function is the topic, it matches with the channel we defined in our socket module, and also what we'll be initializing our channel connection to from React. All we need to do is return :ok, along with the socket info.

handlein/3 takes an event command that is a string, "createdeck", and our deckstring value. By letting us use pattern matching on the string, its easy to to have multiple handlein functions that all perform different operations. Handlein then calls create_deck/1 to save the deckstring to the DB. The client doesn't care since about the operation in this use case so we're just returning :noreply.

defmodule HearthdecksWeb.DeckChannel do
  use HearthdecksWeb, :channel
  alias Hearthdecks.Data

  def join("room", _params, socket) do
    {:ok, socket}
  end

  def handle_in("create_deck", deckstring, socket) do
    Data.create_deck(%{deckstring: deckstring})
    {:noreply, socket}
  end
end

Client Side

Onwards to our React frontend. First thing we need to do is create a new socket.js file, we just need to import Socket from phoenix and add the following. A lot of this is generated by Phoenix as well.

import { Socket } from "phoenix"

let socket = new Socket("/socket")
socket.connect()

export default socket

Our implementation is going to all go in one React component that is handling the deck exports, a better way to do this would be to wrap the functionality in a Higher order Component or in your middleware and then call any pushes to your channel in a reducer.

First off, make sure you import socket from socket.js. We've created setPhoenixChannel() that we're going to call in componentDidMount(), this function is going to call socket.channel, notice we're pass in "room", which matches with our "room" topic in phoenix modules. We're going to stash away channel in our components state so we can reference it when we actually want to transport our data.

Finally, when we are generating a deck string for a user, we grab channel in our components state, and push a message that has our desired event, "create_deck", and the created deckstring. If we wanted to do something based on success or not we could handle that in receive.

// import the socket.
import socket from "../../socket.js"


export default class ExportDeck extends React.Component {
// ... a bunch of other normal component stuff.


// run a function that joins the phoenix channel after the component mounts.
componentDidMount() {
    this.setPhoenixChannel()
}

// create a function that joins the channel and set the channel in the components state.
setPhoenixChannel() {
    let channel = socket.channel("room", {})
    this.setState({ channel: channel })

    channel.join()
}

// whenever a user generates a deckstring we push it to the channel along with the "create_deck" message.
generateDeckString() {
    // ...other deckstring creation stuff.

    this.state.channel
    .push("create_deck", deckstring)
    .receive("ok", resp => {}))
}

}