Phoenix, React, Redux example
Integrating React and Redux with the Phoenix web framework.
Elixir Phoenix JavaScript React Redux
Why Phoenix?
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?
React is a JavaScript library, from Facebook, for building user interfaces. It is commonly thought of as the V in an MVC architecture. It’s stated design goal is to solve one problem: building large applications with data that changes over time. I’ve previously written about my interest in combination with D3 for charting. I may be in the minority of JavaScript developers who embraces the JSX format.
Redux is an implementation of Facebook’s Flux architecture. In their own words, Redux is a predictable state container for JavaScript apps. I became aware of the library and interested to learn more, when a former colleague - James Hollingworth, author of the Flux library Marty - deprecated his own project and recommended 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.
Getting started
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
Installing Phoenix
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
Use the 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
Initial commit after generating new Phoenix app
Configuring front-end dependencies
Create a package.json
file, using the npm init
command. Install the following front-end libraries using npm install
.
Create the webpack configuration file webpack.config.js
to package the JavaScript assets. Use the babel loader including both es2015
and 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 app.js
file.
<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 root
. ES6 import
statements are used for module loading.
Redux/React integration
Integrating Redux followed the Usage with React guide in the Redux documentation.
Actions
Create the action type constants and action creator factory functions in web/static/js/actions.js
.
Reducers
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.
Redux store
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.
Dispatching actions
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 connect
call.
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.
Async dispatch using redux-thunk
middleware
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.
Configure socket in JavaScript
Phoenix provides a JavaScript API to its channels abstration.
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.
The 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.
Configure web socket connection to Phoenix channel
Phoenix channel
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.
Server
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 broadcast!
call.
Client
On the client, configure the socket and connect to the todos
channel by calling the previously defined configureChannel
.
Add a new subscribeTodos
function to handle receiving the new items broadcast from the server.
The 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.
The 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.
Todo socket and channel for realtime TODO notifications
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.
Server
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 (new:todo
).
Client
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 fetchTodosSuccess
.
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.
Elixir agent to persist TODO state in memory
Demo
An example of two users simultaneously connected, with new items being added. Both browsers are refreshed to show that state is persisted between reloads.