avatar

Derek Zeng

A programmer

Angular, Redux, ngrx and complexity management

by coderek

Having a proper way to manage complexity is crucially important in software development. It leads an established way of adding featurs and fixing bugs, so the software can grow as big as you wish while still maintaining the basic simple structure that is easy to reason about. This also ensures that software can be used reliably for a long period of time.

In the past few weeks, while I am working on my reader project based on Angular, this complexity management question bothers me a lot. I have to refactor the software several times, each time trying to come up with a better solution.

Basically the problems I'm trying to solve boils down to the followings:

reader service+-------------------------------------+  +--router+------+
+                                                   |  |               |
| +---------------------+  +--------------------+   |  |               |
| |                     |  |                    |   |  |    - feed     |
| |   storage service   |  |   session service  |   |  |    - entry    |
| |                     |  |                    |   |  |               |
| +---------------------+  +--------------------+   |  |               |
+---------------------------------------------------+  +---+-----------+
                        |                                  |
                        |  injected                        | injected
                        v                                  v

+reader app component+-------------------------------------------------+
|                                                                      |
|  +-----------------------+     +-----------------------+             |
|  |                       |     |                       |             |
|  |    layout component   |     |   feed list component |             |
|  |                       |     |                       |             |
|  +-----------------------+     +-----------------------+             |
|                                                                      |
|  +-----------------------+     +-----------------------+             |
|  |                       |     |                       |             |
|  |   reading component   |     |    toolbar component  |             |
|  |                       |     |                       |             |
|  +-----------------------+     +-----------------------+             |
+----------------------------------------------------------------------+

The million dollar question I have with the diagram above is What's the best way for reader service to interact with the router? This can also extend to how does different services interact with each other. There may be others like websocket service, email service etc.

In the reader service, I keep the application state in it. The app state includes the list of feed/entry objects being displayed.

In the router service, I keep the route related state in it. This includes currently displayed feed/entry.

I want the keep these state in respective service object and not to duplicate them in any way.

In the original way I tried, all the communication/processing are done in the components because it's components who have references to those service singleton, not the service themselves.

One example of such interaction is that when the router navigating to a feed, I have to first take the feed id from the url param then pass it to reader service to fetch the entries for the feed.

This process is asynchronous and roughly like the followings

ngOnInit() {
    this.route.params.subscribe(params=> {
        if (params['feed_id']) {
            let feedId = params['feed_id'];
            this.readerService.getFeed(feedId).then(feed=> this.feed = feed);
            this.readerService.getEntriesForFeed(feedId).then(entries=> {
                this.entries = entries;
            });
        }
    })
}

In this example, I also have to make sure the getFeed method returns the same object reference from previous getFeeds call, so I don't have to maintain multiple copy of the same object that may cause inconsistencies. To implement this, I have to create cache objects in reader service and manage the cache whenever the objects update. Obviously this is messy.

In addition to that, the component is also responsible for passing data passing through inputs/outputs. Allowing data flows up and down makes me feel so wary and out of control. I would like the data flow in a predictable way, like it in redux. But as I described in my previous article and in my opinion, redux is not needed in Angular.

Is it really so?

In the articleManaging states in Angular Applications, Victor points to a popular Angular library called ngrx. I wasn't so sure if I should use it at first. But after I had watched this video, I'm convinced it could make my life much easier.

So the idea now is keeping only one centralized state of everything needed to render the application in browser. The state is actually an observable object. My components now do not need @Input. All it needs is to subscribe to the state observable object and update UI reactively. The subscription is selective like this:

// in the redux file
let state = {
    counter: 1
}


// in the component
.....
template: '{{counter | async}}'
.....
this.counter = store.select('counter');

The @Output is not needed too. We use only redux actions. So when a button is clicked, we don't emit Angular event. We dispatch an action to the global store it then handles the action and make change to the store and the change is propagated throughout the application.

One immediate benefits is that we can start using OnPush change detection strategy in all such components. This is because everything is dependent on the global states now. (Of course, if we decide to still keep some UI related state to the component, then we can't use OnPush)

This is very easy to understand.

Okay, to answer my million dollar question, What's the best way for reader service to interact with the router?

We duplicate router state in the global state and keep them in sync. In fact, router has one way data flow. We click some link and navigate to the route with some new state and trigger the route action with those state in the component.

+------------+          +----------+
|            |          |          |
|click link  +--------> |new route |
|            |          |          |
+----^-------+          +-------+--+
     |                          |
     |                          |
     |   +----------------+     |
     |   |                |     |
     +---+    component   | <---+
         |                |
         +----------------+

Whenever a new route is generated, we dispatch an action to the store. So the reader store now has two fields for router.

let store = {
    ...
    current_feed: '',
    current_entry: '',
    ...
}

Notice that the fields are strign type, because I only want to store the id of the object so it can reference to the real object in the store. Then I make to actions to update the fields.

// reducers
function current_feed(state, action) {
    if (action.type === SET_CURRENT_FEED)
        return action.payload;
    return state;
}
function current_entry(state, action) {
    if (action.type === SET_CURRENT_ENTRY)
        return action.payload;
    return state;
}
export reducer = combineReducers({current_feed, current_entry});

Then in the component, I need to dispatch the action when route changes. Since ngrx store is an rxjs subject of actions, I can just do:

this.route.params
    .map(params=> new SetCurrentFeedAction(params['feed'])
    .subscribe(this.store);

If we have another service to push data to the app, we can do the same and merge that data into the global state.

ngrx is based on redux, but it provides an alternative way to implement async actions. In redux, async actions can be implemented using thunks while in ngrx they are implemented using Effects.

I personally like the Effects very much. Effects separate the concerns of client and server updates. It creates listeners to intersting actions and update other services that if success do not need to update the state again. or so called, optimistic update. This allows very responsive UI. Also since the Effects (like side effects) is separate entities, they are easily testable.

Since I keep a global state now, I free the reader service from keeping internal cache/states, so as to other services. So the services can become stateless. Also my components become very light now, all they are doing is firing actions.

This is my architecture now.

      +----------------------------------------------------+                           +-----redux-----+
      |                                                    +---------actions----------->               |
      |                                                    |                           |               |
      |                    Effects (async)                 |                           |               |
      |                                                    |                           |               |
      |                                                    <--------subscriptions------+               |
      +------------^---------------------------------------+                           |               |
                   |                                                                   |               |
                   |                                                                   |               |
                   |  Injected                                                         |               |
                   |                                                                   |               |
                   |                                                                   |               |
                   |                                                                   |               |
reader service+----+--------------------------------+  +--router+------+               |               |
+                                                   |  |               |               |               |
| +---------------------+  +--------------------+   |  |               |               |               |
| |                     |  |                    |   |  |               |               |               |
| |   storage service   |  |   session service  |   |  |               |               |               |
| |                     |  |                    |   |  |               |               |               |
| +---------------------+  +--------------------+   |  |               |               |               |
+------------------------+--------------------------+  +--+------------+               |               |
                         |                                |                            |               |
                         | injected                       |                            |               |
                         |                                |                            |               |
+reader app component+---v--------------------------------v--+                         |               |
|                                                            |                         |               |
|  +-----------------------+     +-----------------------+   +-------actions---------> |               |
|  |                       |     |                       |   |                         |               |
|  |    layout component   |     |   feed list component |   |                         |               |
|  |                       |     |                       |   |                         |               |
|  +-----------------------+     +-----------------------+   |                         |               |
|                                                            |                         |               |
|  +-----------------------+     +-----------------------+   |                         |               |
|  |                       |     |                       |   |                         |               |
|  |   reading component   |     |    toolbar component  |   |  <-----subscriptions----+               |
|  |                       |     |                       |   |                         |               |
|  +-----------------------+     +-----------------------+   |                         |               |
+------------------------------------------------------------+                         +---------------+
(End of article)