Phoenix + React: love story. RePh 1.

Want to start building RePh app without carrying about scaffolding? Good news: I just released RePh app generator, take a look!

Hi, in this article I’m going to show you, how to create a fullstack app with React and Phoenix. There are already a few similar tutorials out there, however none of them shows process of development for a complete functional app with server-side rendering and routing. The article is about to fill the gap. In the first part we will create a basic (but yet functional) app which will copy standard Phoenix app layout and will include real-time statistics of visitors: number of total, online and max. online users. The app name is RePh which stands for REactPHoenix.

Complete app code is available at Github, every step lives in it’s own commit: https://github.com/chvanikoff/reph

The demo is available at https://reph.herokuapp.com but is only available 18h/day due to Heroku free dyno limitation. If you see `Application Error` message — this is the case.

Primary tech stack used.

  • Elixir 1.2.5
  • Node 4.4.3
  • NPM 3.8.9
  • WebPack 1.13.0
  • Phoenix 1.1.4
  • React 15.0.2

Getting started.

First of all we will create a Phoenix app named reph. I’m using ~/phoenix directory for my Phoenix projects so change it to the one you prefer. During project generation you will be suggested to install dependencies — answer “no”. This will save your time because fetching npm deps usually takes long and we will not need the ones installed by default with Phoenix app generator.

mix phoenix.new ~/phoenix/reph

Once we have a fresh Phoenix app in place, let’s install all deps we will need and clean it up a bit — there’s some stuff we will not need and some we would like to change.

Now let’s create a WebPack config file:

And final step in our setup is to replace Brunch with Webpack in config/dev.exs.

Hello Phoenix.

In this part we will reproduce a simple starter Phoenix page. First look at web/templates/layout/app.html.eex — the whole app is in <div class=”container”></div>. It will be our primary React App container. The web/templates/page/index.html.eex content will be moved to a React component. Remove everything but inner view and JS tag from the web/template/layout/app.html.eex layout file so it looks like this:

and replace all the web/templates/page/index.html.eex file contents with a single div — here we will render our React app:

<div id="index"></div>

Now create App container in containers/App/index.js:

The render() function returns almost the same what Phoenix default layout does. However, don’t forget to replace all “class=” with “className=” while copying HTML to React classes as JSX. Also don’t forget to close all single-tags like <hr/> or <img/> with trailing slash.

The container is a wrapper around our components and it will remain the same for all the routes inside the initial one, “/” in case of our simple app. The only part that will change when route changes will be {this.props.children}.

Now let’s write our only component which will render all the web/templates/page/index.html.eex, the component will be located in components/Main/index.js:

Next thing we need is a router, it will be pretty much simple:

Now let’s define our app state and reducers required. Initially we’ll focus on the basic app without any logic — at this point our primary goal is to make the app render all the same what Phoenix does by default. This means our app’s state will be empty. However, it still must contain routing object in it. Let’s create a simple reducer at reducers/index.js

Next what we need is a store, as long as we are writing a minimal working app, it will be very simple. Create a store/index.js file with following code:

Router is also usually kept in container so we need to create containers/index.js since router is the app’s entry point.

Now it’s time to connect all the parts together, create index.js file which will be an entry point for webpack to compile the app

Last thing to do is to replace embedded into styles/index.less Bootstrap with the NPM dependency. First, we will be able to easily customize it if needed, and second it’s just needless to have there. Open web/static/styles/index.less and replace all the Bootstrap code with this line:

@import "~bootstrap/less/bootstrap";

From here we are good to compile and launch the app. Run

mix phoenix.server

and open http://localhost:4000 to see the basic Phoenix page was rendered with React.

Hello visitors.

To track visitors, we will use a stateful gen_server. It will have 3 API functions we’ll use: add/0 to be called when a client joins, remove/0 to be called when a client leaves and state/0 to provide connected clients with current state. Here is the code for complete server:

To make the server usable, we must add it into our supervision tree. Open lib/reph.ex and add

worker(Reph.Visitors, [])

to the list of app supervisor children, right after the comment “# Here you could define other workers and supervisors as children”.

Now we need a reducer to get this state on client, it will process state initialization and replicate the server’s logic. Create reducers/visitors.js:

and add the reducer to reducers/index.js

Now when we can use the state in our app, let’s update components/Main/index.js, we will use connect from react-redux library to access application state (and visitors in it). New version of the Main component will look like this:

You can run the app and see the state was rendered. However it is useless so far, we must add server-client communication layer to register and unregister client, provide it with initial state and update on new connections. Obviously, we’re going to handle all of this with websockets and I’d suggest to keep websocket and channels in state having another reducer for it. I’ve seen a lot tutorials with WS and it’s callbacks put directly into store or even entry (index.js) file, but I always felt wrong doing this — mostly because of difficult access to the objects when it’s needed. Create a new reducer in reducers/ws.js:

and add it to reducers/index.js:

To connect to websocket and channels, we need to specify certain actions — we will do this with action creators. Create a file actions/ws.js:

Now we can connect to websocket but let’s first create functional channel to connect to. Open web/channels/user_socket.ex and add the following route in it (right after commented “Channels” section):

channel "visitors", Reph.VisitorsChannel

Our VisitorsChannel will be capable of sending current Visitors state and adding a new one on connect, removing visitor from state on disconnect and broadcasting all incoming add/remove event messages to clients. Here is the file web/channels/visitors_channel.ex:

The only thing left on server side is broadcasting event messages when we update Visitors state, let’s extend the module:

Now we are ready to serve our client app so let’s connect it with websocket. Open web/static/js/store/index.js and update it to make look like the following:

Here we go! Launch the app with

mix phoenix.server

and play around with live visitors counter.

Hello server-side rendering.

This part will be culmination of our simple app development. We already have it working, but what we want now is to perform initial rendering on server side. How we are going to handle this? With 2 libraries: react-stdio to compile React app and std_json_io to deliver the result to clients. But before diving into usage of these libraries, we’ll need to make our JS bundle server-friendly. First, we must create a new Webpack config file to create the bundle in a bit different way: it should be compiled as a commonjs2 library and the entry point will be our app index container since the only thing we need the library to return is the component to render. Also we will skip everything but JS. Here is the config for server, save it as webpack.server.config.js

To automatically recompile the server bundle as well as regular one, add a new watcher to config/dev.exs, finally watchers value should be this:

There is some code in our app that should only be executed in browser, like websocket connection. Also, routing must be synchronized between client and server so when you add a new route “/mypage”, you can access it directly, not only from the index page of the app. This means we must update our JS. To determine if the code is running in browser we will simply use typeof window !== “undefined” condition.

First, let’s remove dispatching of WSActions if we are not in browser. Open store/index.js and add the if-condition:

All the routing/initializing logic is determined in our root container — containers/index.js. Remember we have a state in our app? We will pass it to the container on server-side via props attribute and we will set global JS variable __INITIAL__STATE__ to use on client-side even before receiving the one from websocket. State received from WS will just update it if needed. To use the initial state on client, you should pass it to createStore function in store/index.js as second argument. The configureStore function will receive it from Index container:

Now it’s time to update the Index container. It will be responsible for proper requests routing on server and for passing initial state down to configureStore.

It was our final step in client-side, now we need to compile the server bundle on user’s request to send it to him. In a real world I’d use precompiled (cached) bundles which are recompiled on the related state change. One day I will cover this topic as well but now we are still building a simple app and generating the bundle on the fly will work just fine.

Now we need to setup std_json_io, which will take care of managing pool of react-stdio workers and communicating with them. Add {:std_json_io, “~> 0.1”} to list of deps and :std_json_io to list of apps in mix.exs:

run mix deps.get to fetch the library and then create a module in lib/reph/react_io.ex:

This module is a supervisor for react-stdio workers so add it to our supervision tree next to Visitors worker:

supervisor(Reph.ReactIO, [])

In PageController, we are going to include React-generated HTML as well as initial state into template params. Here is how we will do this:

And now we can update page/index.html.eex to embed the generated HTML and initial state into it:

We are passing conn.request_path to our JS but currently we only serve “/” location. To serve all requests with PageController, replace get “/” with forward “/” in your web/router.ex:

Next step is… Hold on, there is no next step! That’s it, go run mix phoenix server and enjoy the fact that you know how to create fullstack apps with Phoenix and React! =)

Hello DevTools.

It is worth nothing to add Redux Devtools for Chrome extension so why not? Since this is a store enhancer, we will need compose utility from redux library to actually compose it with applyMiddleware enhancer. The devtools enhancer will look like following:

const devToolsExt = typeof window === "object" && typeof window.devToolsExtension !== "undefined"
? window.devToolsExtension()
: f => f;

and the complete store/index.js code will be:

Install the chrome extension from the extension page and run the app, you should see Redux Devtools icon turned on, also it should be available from context menu.

Next part

In next part we will improve this app with authorization and different apps for guests and logged in users.

Go to part 2

Show your support

Clapping shows how much you appreciated Roman Chvanikov’s story.