and while I focus primarily on the backend of our application, I also touch the frontend and work with our frontend developers. That work includes common cycles in frontend development: edit, save, refresh. That "refresh" step can take a lot of engineering time and effort for a complex web application due to the length of the feedback cycle. This post is about the ways Hot Reloading helped our team shorten this cycle, and how to apply it on your React apps that are built with Webpack and are using Phoenix as the backend.
Live Reloading, Hot Reloading and Phoenix
The two general approaches to shortening the refresh feedback cycle are: live reloading and hot reloading.
Live Reloading is when the browser automatically does a full page reload after a change is made. This eliminates the need for the developer to alt-tab over to the browser and manually hit `Cmd+r`. Phoenix supports live reload out of the box via Phoenix LiveReload. You can configure Phoenix LiveReload to watch a set of file patterns. When any of the files in those locations change (or a new file is added) Phoenix LiveReload initiates a full page reload. Apps created via mix phx.new have Phoenix LiveReload configured by default.
But, there is a downside to those full page reloads. When developing a highly interactive web application, one of the frictions encountered is that it becomes time consuming to reproduce the state that is needed to test a particular feature after a full page reload. For example, imagine that you're implementing an undo/redo stack; you want to change some wording or styling on an element, but that element only shows up after you have already created some data and then hit undo a couple times. It is time intensive to redo those steps each time you want to make a change. This is where hot reloading comes in. Hot Reloading is when parts of the page update without a full page reload which creates a significant time-savings when building complex web applications.
React Terminology and Caveats
Since we're dealing with Hot Reloading for a React app, some react terminology is in order. The term 'Hot Reloading' used in this post is used in the general sense to refer to changing the content on the page but without a full page reload (the definition of Hot Reloading that I'm using here isn't 100% agreed upon, but it seems to be the predominant usage of the term). Previously Hot Reloading for React was done with a technique called Hot Module Replacement, for technical reasons that approach is no longer used and instead React provides an API for "Fast Refresh." Fast Refresh is what we'll be using.
I’d be remiss if I didn’t mention that Hot Reloading has drawbacks as well. Some code does not play well with Hot Reloading, particularly if you’re changing code that initializes state on the page since that code will not be rerun when the Hot Reload executes.
Also the @pmmmwh/react-refresh-webpack-plugin that we’ll be using is marked as experimental. Also sometimes after running for a while the webpack-dev-server ends up eating gobs of memory and will need to be reset. However, if you keep these caveats in mind and do an occasional manual refresh when needed Hot Reloading can be a powerful tool.
Note: The rest of this post assumes that you're using Webpack 5 and (to a lesser extent) Typescript.
With the release of Phoenix 1.6 many developers are moving on to ESBuild since that is the new default in the Phoenix 1.6 generators. But if you already have a webpack setup that you're happy with it may not be worth it to migrate to ESBuild at this point since ESBuild does not support all of webpack's features (of which Hot Module Reloading is one).
Phoenix generally tries to stay out of the front-end packaging world (hence the move from webpack to ESbuild) and minimize front-end touch points, but there is enough complexity that a blog post about the setup seemed warranted.
Okay, you've sold me, now how do I get this to work?
There's multiple different approaches that can be taken for Fast Refresh but the one that I'll describe today is based on a combination of @pmmmwh/react-refresh-webpack-plugin and the webpack-dev-server.
First in a terminal window in your repo's assets directory run:
We’re going to put the webpack-dev-server in front of our phoenix application in development. This will let Webpack inject a websocket into the page so that it can notify the browser when it is time to do a Hot Reload. To make other developers lives simple we’re going to tell the webpack-dev-server to start on port 4000 and proxy all the requests that it doesn’t know how to handle to the Phoenix server which we’re going to modify to listen on port 4001. So now the browser will still make requests to http://localhost:4000 and they will be transparently routed to Phoenix.
Okay, here comes the scary bit. Now we need to open up assets/webpack.config.js and add the following as top-level keys in the webpack configuration:
Yes there are three different websockets listed. The first is for Phoenix LiveReload, the second is for Phoenix LiveView, and the third is for Phoenix Channels. It's possible that you may have customized the locations of these. For the Phoenix LiveView and Phoenix Channels socket look for any `socket/3` declarations in your MyAppWeb.Endpoint module.
In the loader configuration find your ts-loader (assuming you’re using typescript) configuration and add the `getCustomTransformers` and `transpileOnly` settings (note that the isDevelopment variable was set in the earlier snippet):
Then you need to add the ReactRefreshPlugin to the “plugins” section of your webpack configuration:
Problem: On a fresh build (after nuking `priv/static`) none of the static assets were rendering in development. So remember how in our original setup Webpack would compile files into `priv/static` and Phoenix would serve them? Well, now that Webpack is only building everything in memory via the webpack-dev-server, Phoenix is unable to render our static files.
In the default Phoenix we used the Webpack `CopyPlugin` to copy static files from `assets/static` to `priv/static.`
But with webpack-dev-server by default everything is served out of memory (and it doesn't serve static files) so we need to enable the `writeToDisk` option to actually write the static files out so that Phoenix can serve them. This is why we had to add `writeToDisk: true` to the webpack-dev-server Webpack configuration snippet.
Phoenix Endpoint Configuration
In your Phoenix Endpoint configuration in `dev.exs` change the http port to 4001 but then set the url to 4000. This will instruct Phoenix to bind to port 4001 where it will actually listen for requests, but generate links to port 4000 so they can be correctly served by webpack-dev-server first.
This approach helped our team move quickly through the development process, and get one step closer to being the best place to make a map on the internet. I hope you find it useful in your own pursuits. If you deploy this approach or have an alternative to share, I will be speaking at ElixirConf in Austin, Texas and would love to connect.
Jason is a Senior Software Engineer at Felt. He loves to remove friction from the software development process and is enjoying learning all about maps. He's a member of the ElixirLS and exsync core teams.