Tagged with

Today, we will see how to setup React and Redux in a Phoenix app. We will do this using webpack.

Here are some related episodes you may be interested in:

Creating our Phoenix app

I’m assuming you already have Phoenix and Elixir installed. If you don’t, check out this DailyDrip video.

mix phx.new phoenix_react_redux --no-brunch

Phoenix uses Brunch by default, but we are going to use webpack. So, we create the Phoenix app without Brunch.

Let's cd into our project, and start our server.

cd phoenix_react_redux
mix phx.server

Go to http://0.0.0.0:4000/, the default port for Phoenix. Then we should see a Welcome to Phoenix page.

Our assets folder

When we create a Phoenix app using brunch it creates for us a folder /assets with all our css and js. As we are not using brunch, we need to create this assets folder.

All our css and js will be located in our assets folder. Let's create the files we need to start our application.

mkdir -p assets/{js,css}

touch assets/css/app.scss
touch assets/js/app.js

Setting up webpack

We need to have a package.json and add our front-end dependencies and we want to do that in our assets folder. Let's cd into our assets folder and do a yarn init.

cd assets
yarn init

We will need to add some dependencies to our project. There are a lot of dependencies. Some of these dependencies are related to webpack, others are related to react and redux.

yarn add react react-redux redux redux-thunk webpack webpack-dev-server react-dom immutable copy-webpack-plugin
yarn add babel-loader babel-core babel-preset-es2015 extract-text-webpack-plugin
babel-preset-react style-loader sass-loader import-glob-loader node-sass --dev

Now, let's create a script to run webpack.

vim package.json

We can add a webpack script in our package.json file.

  "scripts": {
    "webpack": "webpack-dev-server --watch --color"
  },

For the moment, we can run webpack separately from our Phoenix application, we will make they work together in the next section. This is just for testing and to see that webpack is working.

If we try to run webpack right now using the script we have just created, we will see it doesn't have a webpack.config file.

We need to have a simple and default webpack configuration. I will use some bits from the webpack configuration Josh did in a previous episode.

vim webpack.config.js

And then we will get our webpack configuration.

var webpack = require("webpack");
var path = require("path");

// We'll be using the ExtractTextPlugin to extract any required CSS into a
// // single CSS file
const ExtractTextPlugin = require("extract-text-webpack-plugin");
// // We'll use CopyWebpackPlugin to copy over static assets like images and
// fonts
const CopyWebpackPlugin = require("copy-webpack-plugin");

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

// We'll set up some paths for our generated files and our development server
const staticDir = path.join(__dirname, ".");
const destDir = path.join(__dirname, "../priv/static");
const publicPath = "/";

module.exports = {
  entry: [staticDir + "/js/app.js", staticDir + "/css/app.scss"],
  output: {
    path: destDir,
    filename: "js/app.js",
    publicPath
  },
  module: {
    loaders: [
      {
        test: /\.jsx?$/,
        exclude: /(node_modules|bower_components)/,
        loader: "babel-loader",
        query: {
          presets: ["es2015", "react"]
        }
      },
      // Any CSS or SCSS files will run through the css loader, the sass
      // loader, and the import-glob-loader. The last one will allow us to use
      // glob patterns to import SCSS files - for instance, a whole directory of
      // them. That isn't available by default in node-sass
      {
        test: /\.s?css$/,
        use: ExtractTextPlugin.extract({
          use: "css-loader!sass-loader!import-glob-loader",
          fallback: "style-loader"
        })
      }
    ]
  },
  // And we'll configure our ExtractTextPlugin and CopyWebpackPlugin
  plugins: [
    new ExtractTextPlugin("css/app.css"),
    // We copy our images and fonts to the output folder
    new CopyWebpackPlugin([{ from: "./static/images", to: "images" }])
  ]
};

Testing if webpack is working

To test if webpack is working, let's just put an alert on the app.js file.

vim assets/js/app.js

And we can add an alert.

alert("This is loaded with webpack")

Let's tell our Phoenix app how to load the js and css from the webpack dev server.

vim lib/phoenix_react_redux_web/web/templates/layout/app.html.eex
<!DOCTYPE html>
<html lang="en">
  <head>
    <!-- ... --/>
    <%= {:safe, css_link_tag()} %>
  </head>

  <body>
    <!-- ... --/>
    <%= {:safe, js_script_tag()} %>
  </body>
</html>

And according to our previous episode from Josh, we can create some functions that helps us to render the css and the js.

vim lib/phoenix_react_reduxl/web/views/layout_view.ex
defmodule FirestormWeb.Web.LayoutView do
  use FirestormWeb.Web, :view

  def js_script_tag do
    if Mix.env == :prod do
      # In production we'll just reference the file
      "<script src=\"/js/app.js\"></script>"
    else
      # In development mode we'll load it from our webpack dev server
      "<script src=\"http://localhost:8080/js/app.js\"></script>"
    end
  end

  # Ditto for the css
  def css_link_tag do
    if Mix.env == :prod do
      "<link rel=\"stylesheet\" type=\"text/css\" href=\"/css/app.css\" />"
    else
      "<link rel=\"stylesheet\" type=\"text/css\" href=\"http://localhost:8080/css/app.css\" />"
    end
  end
end

Now, we can test that! We can run the phoenix server and webpack and visit http://localhost:4000. We should have seen the alert.

Currently, we are running webpack and phoenix separately. We would like to have our Phoenix server manage the webpack dev server. Let's do it!

Adding webpack as a watcher

We can configure the Phoenix app to have watchers, so it runs some command every time files change. We will do that with our webpack configuration.

vim config/dev.exs

And we will add this as the following command.

...
watchers: [
    node: [
      "node_modules/.bin/webpack-dev-server",
      "--inline",
      "--hot",
      "--stdin",
      "--host", "localhost",
      "--port", "8080",
      "--public", "localhost:8080",
      "--config", "webpack.config.js",
      cd: Path.expand("../assets", __DIR__)
    ]
  ]
  ...

This makes our Phoenix application works together with webpack.

Let's try that. Let's shut down our webpack server since Phoenix will run it. To see if our webpack is running correctly, we can change the text in the alert in our assets/js/app.js and run our phoenix server. If we see the new alert text, then we know webpack is running under Phoenix.

vim assets/js/app.js
alert("Webpack loaded");

And now we can run our Phoenix server.

mix phx.server

Opening our browser, we can see the new alert. Now Phoenix is managing our webpack! This is exactly the same test Josh did in a previous episode to make sure webpack was running. If you want to know more details about webpack configuration with Phoenix, check out that episode.

Start Adding React and Redux

Now that we have webpack running, we can focus on the React and Redux bits.

vim assets/js/app.js
import React from "react";
import { render } from "react-dom";
import { createStore } from "redux";
import { Provider } from "react-redux";
import App from "./containers/App";
import reducers from "./reducers";
import MyStore from "./store";

render(
        <Provider store={MyStore}>
            <App />
        </Provider>,
        document.getElementById("root")
        );

Let's create our root div element in the page.

vim lib/phoenix_react_redux_web/templates/page/index.html.eex
<div id="root"></div>

Let’s try to run the Phoenix server and see what it does:

Module not found: Error: Can't resolve './containers/App'

We don't have our App container yet So, it fails looking for this file.

Setting up Redux

Types

We are using Flow. We will have two Types. One type for our model, and another type for our MessageType. Let's check it out.

mkdir assets/js/types
vim assets/js/types/index.js
//@ flow

import Model from "./Model";
import MessageType from "./MessageType";

export { Model, MessageType };

The Model will have a list of Messages.

// @flow
import { Record, List } from "immutable";
import MessageType from "./MessageType";

export default class Model
  extends Record({
    messages: List()
  }) {
  messages: List<MessageType>;
}

The MessageType will have just a text as an attribute.

// @flow

import { Record } from "immutable";

export default class MessageType
    extends Record({
        text: ""
    }) {
    text: string;
}
mkdir assets/js/containers/
vim assets/js/containers/App.js
import React, { Component } from "react";
import { connect } from "react-redux";
import ListMessages from "../components/ListMessages";
import Actions from "../actions";

function App(props) {
  const { messages, sendMessageToChannel } = props;
  let input;
  return (
    <div>
      <ListMessages messages={messages} />
      <input type="text" ref={node => input = node} />
      <button onClick={() => sendMessageToChannel(input.value)}>
        SEND
      </button>
    </div>
  );
}

export const AppContainer = connect(
  function mapStateToProps(state) {
    return {
      messages: state.chat.get("messages")
    };
  },
  function mapDispatchToProps(dispatch) {
    return {
      sendMessageToChannel: message => {
        dispatch(Actions.sendMessage(message));
      }
    };
  }
)(App);

export default AppContainer;

Let's create our component ListMessages.

mkdir assets/js/components/
vim assets/js/components/ListMessages.js
import React from "react";

function ListMessages(props) {
    const { messages } = props;

    const renderMessages = messages.map((message, i) => {
            return <div key={i}>{message.text}</div>;
            });
    return <div className="messages-container">{renderMessages}</div>;
}

export default ListMessages;

Actions

We can start by creating our Actions. We will build something similar to a chat. So, we will have an action called sendMessage.

vim assets/js/actions.js
function sendMessage(message) {
  return {
    type: "SEND_MESSAGE",
    payload: {
      message
    }
  };
}

export default { sendMessage };

Reducers

We will have just one Reducer that we will call MainReducer.

vim assets/js/reducers.js
// @flow
import { Model, MessageType } from "./types";
import { List } from "immutable";
import { combineReducers } from "redux";
const init = new Model();

type ActionType = "SEND_MESSAGE";

function mainReducer(
  model: Model = init,
  action: { type: ActionType, payload: Object }
) {
  switch (action.type) {
    case "SEND_MESSAGE":
      return sendMessage(model, action.payload);
    default:
      return model;
  }
}

function sendMessage(model, payload) {
  if (payload) {
    return model.updateIn(["messages"], messages => {
      return messages.push(new MessageType({ text: payload.message }));
    });
  } else {
    return model;
  }
}

const phoenixApp = combineReducers({
  chat: mainReducer
});

export default phoenixApp;

Store

vim assets/js/store.js

In our Store, we will add redux-thunk and reduxDevTools, if the browser has it.

// @flow

import MainReducer from "./reducers";
import { Model } from "./types";
import { createStore, applyMiddleware, compose } from "redux";
import thunk from "redux-thunk";

const init = new Model();

function composeWithApplyMiddlewares() {
    if (window.__REDUX_DEVTOOLS_EXTENSION__) {
        return compose(
                applyMiddleware(thunk),
                window.__REDUX_DEVTOOLS_EXTENSION__()
                );
    }
    return compose(applyMiddleware(thunk));
}

const createMyStore = (model: Model = init) => {
    return createStore(MainReducer, model,
            composeWithApplyMiddlewares());
};

export default createMyStore(init);

Running it

To run it, we can just run webpack in watch mode and run the Phoenix server.

mix phx.server

Let’s go to the browser. Once we submit an action we can see the redux actions.

Summary

Today, we saw how to set up webpack in our application. This is something we already have seen at DailyDrip, but today was a bit different and we have added React and Redux.

One thing to keep in mind: if you already have Webpack with your Phoenix app, adding React + Redux will be pretty simple.

Resources


33b32be65cd662922a71dc1d051ddb9f?s=184&d=mm

Franzé Jr Software Engineer with experience working in multi-cultural teams, Franze wants to help people when he can, and he is passionate about programming and Computer Science. Founder of RemoteMeetup.com where he can meet people all over the World. When Franze is not coding, he is studying something about programming.