Faster Development with Webpack HMR, Angular 2, and Redux

I’ve been working on an Angular2 app that asks a user to identify themselves and then allows them to navigate through a series of screens. I’m using Webpack, and I’ve enjoyed some of the benefits of webpack-dev-server’s Hot Module Replacement feature. However, I didn’t have things set up quite right. When I wanted to change a deeply nested component (i.e., some screen the user can only view after logging in and navigating to), it was quite frustrating. I’d fire up the app, login, and navigate to the component I was working on. Then I’d make some code change. HMR would kick in, and I’d lose my place in the app. I’d have to start all over again, logging in and navigating back to that screen. Obviously that’s a very inefficient development process. As I explored a solution to this problem, I settled on using Redux and some related middleware to simplify application state management and to react to HMR events such that I do not lose my place when a module is reloaded. This article will give a quick overview of the techniques involved. The source code for this article can be found at https://github.com/sflahave/ngHmr . There are a series of branches, named step1 – step4, which walk you through the problem and solution. I will assume you are somewhat familiar with Angular2, Redux, and Webpack, but provide links to other resources for more information.

Step 1 – Getting Started

To get started, you should clone the source code for this article:

git clone git@github.com:sflahave/ngHmr.git

Then cd into the ngHmr directory, and checkout the branch “step1” with

git checkout step1

In the step1 branch, we have a very basic Angular 2 app and a fairly simple Webpack config. Follow these steps:

  1. Run the app with yarn start or npm start.
  2. Load the app in your browser and open the dev console.
  3. Open app.component.ts in your editor and change the title to whatever you want. Save your changes.
  4. Notice that your changes do not automatically appear. You have to refresh your browser. Bummer! Don’t worry, that’s supposed to happen. We’re going to fix that… Also note that we are using HMR. Actually, here’s the definition of the start script:
   "start": "npm run server:dev:hmr",
   "server:dev:hmr": "npm run server:dev -- --inline --hot",
   "server:dev": "webpack-dev-server --config config/webpack.dev.js --progress --profile --watch --content-base src/",

If you’re not familiar with what’s happening here, I’m simply chaining npm scripts: start is a shortcut for npm run server:dev:hmr, which itself invokes server:dev and passes some extra arguments (–inline –hot). Thus npm start here is equivalent to

webpack-dev-server --config config/webpack.dev.js --progress --profile --watch --content-base src/ --inline --hot

The –hot flag is what enables HMR. You can see in the console out put that something related to HMR is happeneing. But we have to add some code in order to achieve the desired affect. Check out the branch step2.

Step 2 – Enabling Webpack HMR

Now checkout the “step2” branch, where we have a small but important modification to `main.ts`:

if(module['hot']) {
  module['hot'].accept();
}

This chunk of code uses the HMR API. First we check if HMR is enabled and, if it is, we call the accept() method in order to load the modified modules. See the Webpack docs on HMR for details.

To test this out, fire up the app and point your browser to http://localhost:8080 again.
Open up app.component.ts in your editor, and change the title property of AppComponent. Save your changes and
watch them appear in the browser automatically. Awesome!

This is a great step in the right direction, but this app is still way to simple to be interesting. What we really want
is to show how you can navigate around a more complex app, make changes to the component you’re looking at
and have those changes appear automatically without losing your place in the app or your data (your “state”). We don’t want to have to repeat ourselves, firing a specific
sequence of actions on the UI every time we make a change. In Step 3, we’ll add routing and an intentionally sub-optimal approach to state management.

Step 3 – Adding some state

In the “step3” branch, we add a few more components and wire up Angular Router to help with navigation.
We’ll introduce a “counter” component that lets the user click ‘+’ and ‘-‘ buttons to increment or decrement a
counter. The point of this step is to show that while we have HMR set up to reload our modified code, we’re losing
the portion of application state that is held and maintained by the reloaded module(s).

Checkout the “step3” branch and find the file counter.component.ts. Suppose we click the ‘+’ button
five times and then change the CounterComponent code. The CounterComponent will be reloaded, and our counter will be reset to 0.

This probably isn’t the best way to handle state management. We don’t want our state to be affected when we reload a module via HMR.
We don’t want to have to click the ‘+’ button five (or however many) times just to get back to where we were every time we make some code change. We’ll start fixing this situation next, in step 4, by introducing Redux and a couple other associated libraries.

Follow these steps to see what I’m talking about:

  1. Run npm install
  2. Run npm start
  3. When Webpack is ready, open your browser and go to http://localhost:8080
  4. Click the Counter button at the top to navigate to the “counter” component’s content.
  5. Click the ‘+’ or ‘-‘ buttons to change the value of the counter to something other than 0.
  6. In your code editor, open src/app/counter/counter.component.ts. Edit the template. For example, change the text “Here’s a counter, isn’t it cool?” to “Here’s a counter, isn’t it kewl?”. While keeping an eye on the page in your browser, save your changes.
  7. Notice that the page was reloaded, and your counter’s value was reset to 0. That kind of sucks, but it is expected at this point. We’ll fix it in step 4.

 

Step 4 – Adding Redux

In this branch, “step4”, we’ll start to fix the problem of losing application state due to HMR. We’ll ultimately solve this problem with Redux (by way of the ng2-redux library) and a couple other related libraries, such as redux-localstorage. Actually, the important concept here is not really Redux itself, but the ideas behind Redux. Rather than having our application state spread about several components, we’re going to keep it all in a single “store”. The store must be treated as an immutable data structure. To update our application state, we’ll define a reducer function that simply takes the existing state and an action. It makes a copy of the existing state (because our state object must be immutable), and mutates and returns that copy. Exactly how that copy is mutated depends on the particular action being processed. To make things a bit more interesting, we’ll also explore the concepts of “presentational” and “container” components (see Dan Abramov’s article).

So in this branch, we turn CounterComponent into a pure “presentational” component. CounterComponent now represents a counter widget.
Each CounterComponent has a reference to a Counter object, which has properties id, name, and value.  CounterComponent also has controls to increment and decrement the counter’s value.

We also add a CounterList component to act as a “container” for multiple CounterComponents. The user can create several independent counters. As a presentational (aka “dumb”) component, a CounterComponent is given what it needs as inputs, such as the Counter object it is displaying. CounterComponent also emits events when the user clicks on the +/- buttons. These events will be handled by the CounterList container component.

As a pure presentational component, CounterComponent does not maintain any application state internally. It simply has inputs and outputs, and knows how to present it’s particular chunk of application state to the user. Further, it delegates event handling to it’s parent container component.

When the user navigates to the /counter route, it is now the CounterListComponent that takes over (instead of CounterComponent as in step3). If you look at the constructor for CounterListComponent, you can see that it is injected with the application state. Also, it’s template provides a simple form to add a new Counter to the application state.

Creating CounterComponents
The CounterListComponent.addCounter() method may be a bit counter-intuitive. It does not simply create a new instance of CounterComponent and add it to some list. Instead, we view a “counter” as part of the application state. So we dispatch an action, which our Redux-based reducer function ultimately handles. In this case, we’re adding a new “counter”, with it’s own name and current value, to the overall application state (i.e., the “store”). I’ve defined an interface named AppState that describes the shape our app’s state tree (see src/app/store/state.types.ts ). We just have a single property, counters, which is a list of Counter objects.

When a new counter is added, the CounterListComponent.addCounter() method dispatches a “CREATE” action via CounterActions.create() to Redux. Redux then invokes our rootReducer function (in src/app/store/store.ts ), which creates a new Counter object and adds it to our application state tree. This is basic Redux stuff – see the Redux docs for more info about how that all works.

So how DO we actually instantiate a CounterComponent? Well, take a look at CounterListComponent‘s template. There is a section there that simply iterates over a counters$ property. The way that property is defined may seem odd:

@select() counters$: Observable

We’re using the @select decorator from ng2-redux to obtain an RxJS Observable which allows us to observe the counters state property. Anytime our counters state property changes, our CounterListComponent template is automatically updated.

The template uses *ngFor=”let counter of counters$ | async” to iterate over the counters and renders a CounterComponent for each. Notice that since CounterComponent is a purely presentational component, we can use Angular’s ChangeDetectionStrategy.OnPush change detection strategy, which is a much faster and efficient strategy for reacting to state changes (see Victor Savkin’s article for more info).

When the user increments or decrements a particular counter, the CounterComponent emits the appropriate event back to CounterListComponent which then dispatches an appropriate action to Redux in order to update the appropriate counter in our application state. Redux invokes our reducer function, which updates the app state appropriately. The counters$ observable in CounterListComponent kicks in to trigger a UI update.

One awesome benefit of all this rework becomes clear when you realize that our application’s entire state is now represented in a single object which can be serialized and deserialized easily. For example, we can convert it to a JSON string and save it in localstorage, read it back and parse it, and bang, we’re right back where we started. That’s precisely
what we do with the help of redux-localstorage and some tweaks to the Webpack HMR-handling code (you should compare this branch with the step3 branch to see what’s been changed). Now, when we’re notified that a module is about to be reloaded, we can save our current state down to local storage and then reload it after the updated module has reloaded. It’ll be like nothing ever happened – our state is unaffected by changes to component code. That’s awesome – it allows us to play around with a deeply nested presentational component (as we try to make it look just right, for example), without losing our place. That’s a huge productivity booster.

To close, I’d like to recommend that you look into using ImmutableJS as well. It can sometimes be difficult to make sure you’re never mutating the state object in your reducers. A library like ImmutableJS helps with that. However, it can be a bit awkward to use ImmutableJS with Typescript. I found the typed-immutable-record library helpful. You should also play around with the Redux dev tools, which let you arbitrarily move back and forth across the various representations of your app state.

Leave a Reply

Your email address will not be published. Required fields are marked *

*

*