
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