Wiring Up Phoenix Channels with React Hooks
By: Erin Greenhalgh
Tags: JavaScript, Phoenix Framework, Reactjs, Elixir, React Hook

Photo by Brook Anderson on Unsplash

At Fast Radius, we’re building responsive and performant applications in Elixir + Phoenix and React. We’ve leveraged Phoenix channels to help improve our UX.

What are channels?

Channels are a way to send messages in real time between server and client over a persistent connection. Channels make use of Phoenix’s built-in websocket capabilities, so each connection runs in its own process and maintains its own state. This is a big difference from communication via HTTP, which is a stateless connection that only persists as long as the request and response cycle.

Example use-case

A good part of our business involves uploading 3D part files (usually in .STL format) for our mechanical engineers to analyze and help our customers determine the best manufacturing process. Depending on the part, the best process might be traditional, like injection molding, or additive, like Carbon® digital light synthesis (DLS).

In our application, we want an easy-to-scan display of part file details, including a screenshot of the part. We process screenshots through a separate server process, so the image is not always available when we fetch part data. Instead of polling the server with an HTTP request, we use a channel to push the screenshot to the client when it is available.

How To: Connect to Channels from the Front End

Here’s our team’s step-by-step process for utilizing sockets and channels on the frontend.

This article references React’s Context and Hooks API, which are available in React 16.8 and above. If you are unfamiliar with Context and Hooks I recommended getting started with the React docs.

Step 1: Create a connection to a socket, and make that socket available to any component that needs it.

Phoenix.js is Phoenix’s JavaScript client for channels, and it gives us an API for creating and connecting to a socket:

const socket = new Socket('/socket', { params }); socket.connect();

Let’s create a component that leverages React’s Context API in order to make this socket connection available to any component that needs it, no matter how deep in the DOM tree:

Note that we’re storing the openSocket in state so that we don’t create multiple socket connections. In our codebase, we also scope the socket connection to the logged in user to ensure that we don’t have multiple connections per user.

Step 2: Subscribe to a channel on the socket, and make that channel available to a component.

Again, Phoenix.js makes it easy for us to join a channel on a given websocket:

const phoenixChannel = socket.channel(channelName, { params }); phoenixChannel.join();

Now we’ll create a custom hook to encapsulate the channel logic and use in any component:

In our useEffect , we create the channel from our socket. This hook uses the PhoenixSocketContext to give us access to the socket. We join the channel, and upon getting a success response, we set state in this hook to the channel we just joined.

By returning a function from our useEffect we define behavior that will happen right before the component using this hook unmounts (think componentWillUnmount in previous React versions). Before unmounting, we leave the channel. No need to keep listening when our component is no longer on the DOM.

Finally, we return the channel from our hook so a component can access it.

Step 3: Listen on the channel and display data.

Here’s how the hook looks when we use it in a component that renders some part data:

There are two calls to useEffect in this component to interact with the channel. One pushes a message to the channel, sending the part id so the server can start preparing the screenshot. Notice that we only want this to happen on initial render, so we pass an empty array as the second argument to useEffect . The other call listens for a message from the channel and sets state when it receives data.

In our JSX, it’s easy to display the screenshot if we have it from the server or show a loading state otherwise. We can still show all the part data that we already have, say, from an earlier API call.

Takeaways

The benefits of this setup, with the SocketContext and useChannel hook, is that our component doesn’t need to know anything about connecting to the socket or channel. It can push messages and listen for data on whatever channel it needs, and it doesn’t need to worry about polling the server to see if the data it wants is ready.