Render React with Phoenix
Over the holiday period this year I was lucky enough to stay in Tahoe for a week. The kids love a white Christmas. While I was there, @mjackson and his family came up for a couple days. Sledding during the day, and coding at night, what started as an impromptu Phoenix tutorial quickly became an idea between the two of us for server side rendered React with Phoenix. This post is to show what we came up with.
In pretty sort order Michael came up with react-stdio a simple javascript server that will require a file and render it as a React component over STDIO/STDOUT. It took me a little longer to come up with the goods for the Phoenix side, mostly because this was my first real foray into OTP.
Enough already. How?
Ok, so for those who are impatient, you can see a demo application at hassox/react_phx_stdio. If you want the gory details read on.
Before we proceed
I've only got this working with webpack, so if you want to follow along, you'll need your application to use it. I followed http://matthewlehner.net/using-webpack-with-phoenix-and-elixir/ to get setup (mostly).
Also bear in mind that I'm a React n00b so don't expect to be wowed on that front.
The general overview
So, what we're going to do is use React in our app.js. The main component that we're going to use we'll need to pass to our react-stdio process on the server side. We'll get webpack to generate a bundle of just the component, and compile it to a server location so we can pass it to react-stdio from our view.
We're going to start up a pool of react-stdio servers and on each request, we'll pass the component bundle location and the props that the component needs to render it. We'll include the props on the page and use that to render over the top (react will notice and not render stuff).
The code
First, lets get the javascript building the server side asset. We'll hook into Phoenix's watchers to run a separate webpack in development.
Server javascript
We'll get webpack to write a component bundle to a server location to pass to react-stdio.
The server side webpack.server.config.js
looks like:
module.exports = {
entry: {
component: "./web/static/js/components/main.js",
},
output: {
path: "./priv/server/js",
filename: "component.js",
library: "myComponent",
libraryTarget: "commonjs2"
},
module: {
loaders: [{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel",
query: {
presets: ["es2015", "react"]
}
}],
},
resolve: {
alias: {
phoenix_html:
__dirname + "/deps/phoenix_html/web/static/js/phoenix_html.js",
phoenix:
__dirname + "./deps/phoenix/web/static/js/phoenix.js"
}
}
};
We still need to be able to render our component so we need to make sure all the right requires are present. Notice that we told the output to be a commonjs2 library. That's an important step!
For reference, my package.json
looks like:
{
"repository": {},
"dependencies": {},
"devDependencies": {
"babel-core": "^6.3.26",
"babel-loader": "^6.2.0",
"babel-preset-es2015": "^6.3.13",
"babel-preset-react": "^6.3.13",
"bootstrap": "^3.3.6",
"copy-webpack-plugin": "^0.3.3",
"css-loader": "^0.23.1",
"extract-text-webpack-plugin": "^0.9.1",
"file-loader": "^0.8.5",
"react": "^0.14.5",
"react-dom": "^0.14.5",
"react-stdio": "^2.0.6",
"style-loader": "^0.13.0",
"url-loader": "^0.5.7",
"webpack": "^1.12.9"
},
"scripts": {
"compile": "webpack -p"
}
}
Lets get a simple react component into web/static/js/components/main.js
import React from 'react'
const { string } = React.PropTypes
const HelloWorld = React.createClass({
propTypes: {
message: string.isRequired
},
getDefaultProps() {
return {
message: "The default message"
}
},
render() {
const { message } = this.props
return (
<p>{message}</p>
)
}
})
export default HelloWorld
Ok so we have our little component ready. We need to get webpack building it into the location where we can give it to react-stdio. In your config/dev.exs
we'll add a watcher to compile it.
watchers: [
{"node", [
"node_modules/webpack/bin/webpack.js",
"--watch-stdin",
"--progress",
"--colors"
]
},
{"node", [
"node_modules/webpack/bin/webpack.js",
"--watch-stdin",
"--progress",
"--colors",
"--config",
"webpack.server.config.js"
]
},
]
These are almost the same command but one uses webpack.config.js
and one has webpack.server.config.js
.
One thing to note: I had some trouble getting two watchers with the same key when using atoms. Phoenix seemed to collapse the two into one as a Keyword list. By using string keys this collapsing doesn't happen.
Restart your server and you should see your component.js
file build.
That's most of the JS part sorted for now. We'll do the server part and then finish wiring it together to get React to hijack the pre-rendered page.
Server setup
At this point, we need to get std_json_io
installed into our application.
def application do
[applciations: [...., :std_json_io]]
end
def deps
[#snip
{:std_json_io, "~> 0.1.0"},
]
end
Run a quick mix deps.get
and we're ready to get this on the road.
In your lib, lets create a supervisor for all those react-stdio processes.
For my app this is in lib/react_phx_stdio/react_io.ex
:
defmodule ReactPhxStdio.ReactIo do
use StdJsonIo, otp_app: :react_phx_stdio, script: "react-stdio"
end
Make sure you npm install react-stdio before you go any further.
Then we need to add that to our supervision tree. In lib/react_phx_studio.ex
(your application file):
def start(_type, _args) do
import Supervisor.Spec, warn: false
children = [
# snip
supervisor(ReactPhxStdio.ReactIo, [])
]
opts = [strategy: :one_for_one, name: ReactPhxStdio.Supervisor]
Supervisor.start_link(children, opts)
end
When we restart the server now it'll fire up a bunch of fresh react-stdio servers ready to do your bidding.
Lets get something on the page
Ok so almost all the pieces are in place to get some stuff on the page. We've got our server component bundle, our supervisor running a bunch of react-stdio processes and a simple component. Next we'll add a route to spit this out to the browser.
We'll create a single page controller and view to handle this for us.
defmodule ReactPhxStdio.SinglePageController do
use ReactPhxStdio.Web, :controller
def index(conn, params) do
render conn, "index.html", message: Map.get(params, "message")
end
end
Nothing to see here. Lets add it to the router.
scope "/", ReactPhxStdio do
# snip
forward "/app", SinglePageController, :index
end
The forward call in the route is a globbing matcher. It will forward all urls starting with /app
to the index
action.
The view is where the interesting part happens:
defmodule ReactPhxStdio.SinglePageView do
use ReactPhxStdio.Web, :view
def render("index.html", opts) do
props = %{}
if opts[:message], do: props = Map.put(props, :message, opts[:message])
result = ReactPhxStdio.ReactIo.json_call!(%{
component: "./priv/server/js/component.js",
props: props,
})
render "app.html", html: result["html"], props: props
end
end
The interesting part is the json_call!
. That sends the object to the react-stdio server. The :component
key is the location of the file relative to app root, and the props, well they're the props to send down. This is the current API of react-stdio. We're just following that.
Once we have the result, we'll push it to a template to render it and the props onto the page.
web/templates/single_page/app.html.eex
<div id='content'><%= raw @html %></div>
<script>APP_PROPS=<%= raw Poison.encode!(@props) %></script>
If you restart your server now and visit localhost:4000/app you should see the rendered component.
There's only one thing left to do now. Push down our component in our app.js so that it hijacks that pre-rendered react page.
In our template, we had a div with an id of content
and we also splatted out the props into APP_PROPS
. Lets update our app.js to take advantage of that.
import "phoenix_html"
import React from "react";
import ReactDOM from "react-dom";
import HelloWorld from "./components/main";
var App = React.createFactory(HelloWorld);
ReactDOM.render(App(window.APP_PROPS), document.getElementById('content'));
You shouldn't need to restart your server, it should already have refreshed.
UPDATE: I forgot to orignally put this in, but in development, you're going to want to have your react-stdio
servers pick up changes to your server bundle. std_json_io
has you covered on this. You can let it know to watch files and when it notices that one has changed it will kill the react-stdio
servers, the supervisor will start them back up - fresh and ready to go. Drop this into your config/dev.exs
(Please don't add this to any other environments. That would not be cool)
config :react_phx_stdio, ReactPhxStdio.ReactIo,
watch_files: [
Path.join([__DIR__, "../priv/server/js/component.js"]) # only watch files in dev
]
Rendering react on the server this way feels pretty good to me. We have a couple of very loosely coupled components working together to pre-render. For my first try at OTP I'm really happy with how it turned out. I love that if there's a problem with the JS and it dies, it will just start straight back up fresh.
Huge props to Michael. It was amazing to see just how fast he got react-stio up and running.