How to add a JS frontend to an API-only Phoenix app
If you generated a Phoenix app with the --no-brunch
option you probably needed an API backend app.
What if, with your app growing, you realize you’d like to add also some frontend code? This short how-to will show you how integrate a Webpack based app with support to Typescript, React and Sass.
If, instead, you have a standard Phoenix app (with Brunch support) and you want to switch to webpack, then read my previous article.
Create the Webpack scaffolding
To begin, let’s create some initial files in the frontend
folder (which
needs to be created):
mkdir -p frontend/src
cd frontend/
npm init
touch src/index.tsx
touch src/style.scss
All future commands are supposed to be ran from within the frontend/
folder.
The npm init
helped you create an initial package.json
file. It is now time to add some required packages.
Webpack
yarn add -D webpack webpack-cli
Babel
yarn add -D babel-loader @babel/core @babel/preset-env
Sass and CSS
yarn add -D style-loader css-loader sass-loader node-sass
Typescript
yarn add -D typescript awesome-typescript-loader
React and its typings
yarn add react react-dom
yarn add -D @types/react @types/react-dom
Webpack’s extract-text plugin (I’m using @next
version because of an annoying bug in the stable version):
yarn add -D extract-text-webpack-plugin@next
Don’t forget to add the node_modules
folder to your .gitignore
file or you’ll commit a lot of useless files:
echo node_modules > ../.gitignore
Also create Typescript config and linting files:
tsconfig.json
{
"compilerOptions": {
"allowJs": true,
"baseUrl": "js",
"jsx": "react",
"module": "commonjs",
"moduleResolution": "node",
"noImplicitAny": true,
"outDir": "ts-build",
"sourceMap": false,
"target": "es5"
},
"include": [
"./src/**/*"
]
}
tslint.json
{
"extends": ["tslint:recommended", "tslint-react"],
"rules": {
"no-console": [false]
}
}
If, like me, you’re running an editor that supports linting and you’ll run it from the main app
folder, you probably want to symlink the tslint.json
from there (or configure the editor linter
to find it inside the frontend/
folder).
Final step is to create the webpack configuration with the frontend/webpack.config.js
file:
const env = process.env.NODE_ENV
const path = require('path');
const ExtractTextPlugin = require("extract-text-webpack-plugin");
module.exports = {
entry: ['./src/index.tsx', './src/style.scss'],
output: {
path: path.resolve(__dirname, "../priv/static"),
filename: "js/app.js"
},
resolve: {
extensions: [".ts", ".tsx", ".js", ".jsx"],
modules: ["deps", "node_modules"]
},
module: {
rules: [{
test: /\.scss$/,
use: ExtractTextPlugin.extract({
use: [{
loader: "css-loader",
options: {
minimize: true,
sourceMap: env === 'production',
}
}, {
loader: "sass-loader",
options: {
includePaths: [path.resolve('node_modules')]
}
}],
fallback: "style-loader"
})
}, {
test: /\.tsx?$/,
loader: "awesome-typescript-loader"
}, {
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
use: {
'loader': 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}]
},
plugins: [
new ExtractTextPlugin({
filename: "css/app.css"
})
]
};
At this point the frontend pipeline is ready to be executed. From the frontend
folder, run
webpack --watch-stdin --progress --color
and, hopefully, your app will be correctly compiled.
Run the pipeline with Phoenix server
We want to run the pipeline when we execute mix phx.server
. This can be done editing the config/dev.exs
file:
watchers: [
node: [
"node_modules/webpack/bin/webpack.js", "--watch-stdin", "--progress", "--color",
cd: Path.expand("../frontend", __DIR__)
]
]
React “Hello, World!”
Our scaffolding should work, to verify it, let’s create a simple React “Hello, World!”.
It’s simple enough to only be contained in the frontend/src/index.tsx
file:
import * as React from "react";
import * as ReactDOM from "react-dom";
const MainComponent = () => (<div className="hello-world">Hello, World!</div>);
ReactDOM.render(
<MainComponent />,
document.getElementById("app")
);
The template file at lib/<app>_web/templates/layout/app.html.eex
must be edited adding the react entrypoint <div id="app" />
.
It’s done, run mix phx.server
and profit:
mix phx.server
[info] Running <app>.Endpoint with Cowboy using http://0.0.0.0:4000
10% building modules 1/1 modules 0 active
webpack is watching the files…
...
[debug] Live reload: priv/static/js/app.js
[debug] Live reload: priv/static/css/app.css
...