← More

Updated Elixir Phoenix 1.1.4 with Webpack and React Hot Module Reload

Updated instructions for Phoenix 1.1.4 and custom Webpack dev server

Generate your new app without brunch:

$ mix phoenix.new my_app --no-brunch
$ cd my_app

This is file that we want to be able to compile, web/static/js/index.js:

// Phoenix' dependencies
import "../../../deps/phoenix/priv/static/phoenix";
import "../../../deps/phoenix_html/priv/static/phoenix_html";

// Shiny new, hot React component
import React, { Component } from "react";
import { render } from "react-dom";

class Root extends Component {
  render() {
    return <h1>omg so hot</h1>;
  }
}

render(<Root />, document.getElementById("root"));

Install the required js dependencies using npm:

$ echo '{"private": true}' > package.json
$ npm install --save babel-core babel-polyfill babel-loader babel-preset-es2015 babel-preset-react react react-dom webpack
$ npm install --save-dev webpack-dev-middleware webpack-hot-middleware express cors babel-preset-react-hmre babel-preset-stage-0 babel-preset-es2015

A lot of stuff, right?

We need a webpack config. Here’s webpack.config.js:

var path = require("path");
var webpack = require("webpack");
var publicPath = "http://localhost:4001/";

var env = process.env.MIX_ENV || "dev";
var prod = env === "prod";

var entry = "./web/static/js/index.js";
var hot = "webpack-hot-middleware/client?path=" + publicPath + "__webpack_hmr";

var plugins = [
  new webpack.optimize.OccurrenceOrderPlugin(),
  new webpack.NoErrorsPlugin(),
  new webpack.DefinePlugin({
    __PROD: prod,
    __DEV: env === "dev",
  }),
];

if (env === "dev") {
  plugins.push(new webpack.HotModuleReplacementPlugin());
}

module.exports = {
  devtool: prod ? null : "cheap-module-eval-source-map",
  entry: prod ? entry : [hot, entry],
  output: {
    path: path.resolve(__dirname) + "/priv/static/js",
    filename: "index.bundle.js",
    publicPath: publicPath,
  },
  plugins: plugins,
  module: {
    loaders: [
      {
        test: /\.jsx?$/,
        loaders: ["babel"],
        exclude: path.resolve(__dirname, "node_modules"),
      },
    ],
  },
};

And we need a node server for development, webpack.dev.js:

#!/usr/bin/env node
var express = require("express");
var webpack = require("webpack");
var config = require("./webpack.config");

var compiler = webpack(config);
var app = express();
app.use(require("cors")());

app.use(
  require("webpack-dev-middleware")(compiler, {
    noInfo: true,
    publicPath: config.output.publicPath,
  }),
);

app.use(
  require("webpack-hot-middleware")(compiler, {
    log: console.log,
  }),
);

app.listen(4001, "localhost", function (err) {
  if (err) return console.error(err);
  console.log("dev server running on localhost:4001");
});

// Exit on end of STDIN
process.stdin.resume();
process.stdin.on("end", function () {
  process.exit(0);
});

Allow it to be executed:

$ chmod +x webpack.dev.js

Babel 6 needs a .babelrc so let’s add it:

{
  "presets": ["es2015", "react", "stage-0"],
  "env": {
    "development": {
      "presets": ["react-hmre"]
    }
  }
}

Make your app run that script as a watcher in dev. config/dev.exs:

config :speaker, MyApp.Endpoint,
  http: [port: 4000],
  debug_errors: true,
  code_reloader: true,
  check_origin: false,
  watchers: [{Path.expand("webpack.dev.js"), []}]

# ...

config :speaker, MyApp.Endpoint,
  live_reload: [
    patterns: [
      # ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$},
      ~r{priv/gettext/.*(po)$},
      ~r{web/views/.*(ex)$},
      ~r{web/templates/.*(eex)$}
    ]
  ]

And finally add this to the bottom of web/templates/layout/app.html.eex:

<%= if Mix.env == :dev do %>
  <script src='http://localhost:4001/index.bundle.js'></script>
<% else %>
  <script src="<%= static_path(@conn, "/js/index.bundle.js") %>"></script>
<% end %>

And web/templates/page/index.html.eex is just:

<div id="root"></div>

Building for production

Add lib/mix/tasks/digest.ex:

defmodule Mix.Tasks.MyApp.Digest do
  use Mix.Task

  def run(args) do
    Mix.Shell.IO.cmd "NODE_ENV=production ./node_modules/webpack/bin/webpack.js -p"
    :ok = Mix.Tasks.Phoenix.Digest.run(args)
  end
end

And in mix.exs:

defmodule MyApp.Mixfile do
  # ...
  def project do
    [ # ...
      aliases: ["phoenix.digest": "my_app.digest"]]
  end
  # ...
end