Phoenix is a framework for building HTML5 apps, API backends and distributed systems. Written in Elixir, you get beautiful syntax, productive tooling and a fast runtime. – phoenixframework.org
My introduction to Phoenix came with their announcement of v1.0.
Reading about the framework, discovering it’s usage of the Elixir language and Erlang’s VM, and watching videos of its capabilities reminded me of the enthusiam I had experienced for Ruby on Rails. Elixir is often described as Erlang for Rubyists. Hence my immediate affiliation for the language.
Guiding my discovery of the Elixir language was the excellent “Programming Elixir” by Dave Thomas, from Pragmatic Programmers. A similarly titled “Programming Phoenix” by Chris McCord - author of the framework - is also available (currently in Beta format). Both are highly recommended resources.
Why React and Redux?
Elixir and Redux both have an emphasis on functional programming and immutable data. They appear well suited together.
This guide demonstrates the integration of React, Redux and Phoenix channels using a TODO example. The functionality is basic; but can be used as a prototype for more complex applications.
After each section, a link is provided to the corresponding Git commit (on GitHub).
A demo is shown at the end of the article.
If you want to run the example Phoenix application, you will need the following prerequisites.
- Install Elixir and Erlang
- Install Node.js and npm
- Clone the phoenix-react-redux-example Git repository
Phoenix v1.0.3 was the latest version at the time of publishing.
mix archive.install https://github.com/phoenixframework/phoenix/releases/download/v1.0.3/phoenix_new-1.0.3.ez
Creating a new Phoenix project
phoenix.new mix command to create a new project. We’ll be using webpack instead of Brunch for front-end asset packaging. Ecto is also excluded as this example does not require a database.
mix phoenix.new phoenix_react_redux_example --no-brunch --no-ecto
Install Phoenix depedencies and start the server.
cd phoenix_react_redux_example mix deps.get mix phoenix.server
Configuring front-end dependencies
package.json file, using the
npm init command. Install the following front-end libraries using
Create the webpack configuration file
react presets to add support for ES6 and React’s JSX format.
Webpack is configured to load
web/static/js/index.js and output to
priv/static/js. This is served as a static asset by Phoenix.
webpack --watch --color
The main Phoenix layout template (
web/templates/layout/app.html.eex) contains a
script tag to load the single concatenated
<script src="<%= static_path(@conn, "/js/app.js") %>"></script>
Basic React application
To demonstrate the front-end asset pipeline has been correctly configured I included the basic React TODO example.
The entry file
web/static/js/index.js loads the React container App component and renders it to the element with the id
import statements are used for module loading.
Integrating Redux followed the Usage with React guide in the Redux documentation.
Create the action type constants and action creator factory functions in
Write the reducer functions in
web/static/js/reducers.js to handle these actions and return new state in response.
A reducer is a pure function that takes the previous state and an action, and returns the next state:
(previousState, action) => newState. Since the reducer is pure, it must not mutate it’s arguments.
Each reducer is combined together using the
combineReducers function from Redux.
Create a store using
createStore from Redux and pass it the combined reducers.
Wrap the React container
App component with the react-redux
Provider and the created Redux store.
Redux recommends that only container components are aware of Redux (so-called “smart” components). Whereas presentation components are “dumb” and should have no depedencies on the rest of the application or stores.
The container App component is provided with a
dispatch function, via
this.props.dispatch, that is injected via the
Dispatch calls are passed as
props to “dumb” child components. The action creator functions are used to provide arguments to dispatch.
Redux is now fully integrated with React in the example TODO application.
Async actions using Thunk middleware for Redux
Redux Thunk middleware (redux-thunk) allows you to write action creators that return a function instead of an action. We can use this to add support for asynchronous action creators to Redux (since it only has support for synchronous action creators).
The store is created and wrapped with the thunk middleware, before being passed to the Redux
Provider as before.
Sending requests to an external service, e.g. a REST API or via a web socket connection, requires three action types. The request, a success response and a failure response.
The request action type can be used to allow the UI to render a pending state.
Phoenix sockets (web sockets) and channels
Channels are the Phoenix abstraction around Web Sockets - providing real-time streaming - allowing you to create interactive, multi-user web applications. Since Phoenix runs on the Erlag VM, it can support a high number of simultaneous connections. A recent blog post titled “The Road to 2 Million Websocket Connections in Phoenix” confirms its performance.
Phoenix provides an
Endpoint module to configure any socket handlers.
To import the
Socket type from the Phoenix client library, add an alias to the
webpack.config.js file that maps to the
phoenix.js file in the top-level
deps folder. This folder contains all the dependencies installed with mix, including Phoenix.
Socket is used to connect to the server, via a web socket, and join channels.
With these client changes made, refreshing the web browser will initiate a permanent web socket connection to the server.
Channels handle events from clients and connections persist beyond a single request/response cycle. Channels are the highest level abstraction for realtime communication components in Phoenix.
Given the following socket definition with a single
todos:* channel route (
* matches anything).
The corresponding channel handles new clients joining and receiving a
new:todo push message from any client.
In response, all connected clients are notified of the new TODO message via the
On the client, configure the socket and connect to the
todos channel by calling the previously defined
Add a new
subscribeTodos function to handle receiving the new items broadcast from the server.
addTodo action creator function is modified to push the new TODO text to the server via the configured channel. Only errors need to be handled by the push call. The newly created item will be received via the
new:todo subscription and dispatched.
App component dispatches the action to subscribe to the new items when the component is mounted.
With these changes made, mutliple users can connect to the server and receive notifications of newly added TODOs while the page is open. The next step is to add persistence to the TODO list.
Elixir Agent for persistence
Agents are a simple abstraction around state. – elixir-lang.org
The Elixir agent simply stores a list of items in memory. It provides an API to get all the items and add a new item.
This agent is added to the Phoenix application’s supervisor so that it is started with the server.
Integrating the agent into our channel is straightforward. When a client joins the
todos channel they receive the entire list of items. When a new item is pushed to the server, it is added to the agent’s state and broadcast to all connected clients (
On the client, we intially fetch the items by joining the
todos channel. The
messages received - containing the list of items - once connected is dispatched through
The reducer function returns the new state. Creating a new, empty array concatenated with the list of items.
Redux and React handle updating the UI in response to the state change.
That completes the end-to-end example of a multi-user TODO application using React, Redux and Phoenix channels.
An example of two users simultaneously connected, with new items being added. Both browsers are refreshed to show that state is persisted between reloads.