Angular Components and the Redux Store: Now You Can Have Your Cake and Eat it Too!

Over the last year, two architectural ideas have risen to the surface of JavaScript web app development: Component-Oriented Architecture (COA) and Redux state management. Component-Oriented Architecture is one of the main tenets in both React and Angular: it encourages developers to break down the UI into a graph of self-contained, re-usable UI components. On the other hand, Redux is a functional-reactive approach to state management where the UI is at any time a derivation of a global, immutable store.

On the surface, these ideas seem to be at odds with each other when it comes to UI components that need to interact with state in order to function. COA seems to suggest that this state be encapsulated inside the component that uses it, whereas Redux's global store seems to imply exactly the opposite approach. This blog post explores this apparent tension and suggests an alternate approach using some new features of @angular-redux/store.

Example

We'd probably want to define a "tabset" UI component that selectively renders some content based on which tab has been chosen. We'd probably also want to be able to use this component a bit like this:

<demo-tabset
title="My TabSet Instance">
<demo-tab title="The First Tab">
Content to be shown when the first tab is clicked.
</demo-tab>
<demo-tab title="The Second Tab">
Content to be shown when the second tab is clicked.
</demo-tab>
</demo-tabset>

A common way to implement something like this in Angular is to use projection and @ContentChildren:

This is nice and simple, and gives us the main benefits of Redux: all the state is in the store so it's subject to Redux's powerful dev tools. In particular:

We can play the application backwards and forwards and the tabset will respond accordingly.

If someone finds a bug, they can export the action stream from Redux DevTools and we can load it into your own browser to play the app forward to the exact point of the failure.

However, we run into problems as soon as a web app wants to have multiple tabsets on the page at the same time. Because the state is global, is it also shared; selecting a tab in one instance of the tabset will change tabs in all other tabsets on the page as well!

In the Component?

In the non-Redux world, we'd just make the active tab a member variable on the tabset component:

This works great: activeTabId is neatly encapsulated inside the instance of the tabset to which it belongs, so we can have many tabsets on the same page, and they all work independently.

However, we've lost many of the benefits of Redux. Because the active tabs are no longer kept in the global store, we can no longer time travel over them in Redux DevTools. If we have a weird layout bug that only happens when four tabsets on our page are in a specific state, we have to get the precise reproduction steps from QA and repeat the bug manually rather than importing an action script and replaying the app to the right spot automatically. Once we've been able to do that, going back to click-click-and-click can be pretty painful.

The Best of Both Worlds

We're faced with a conundrum: Redux's powerful debugging tools work best when as much of your application state is kept in the store as possible; state hidden inside component instances does not come under the purview of the development tools. This means that stateful components need to be debugged the old-fashioned way. On the other hand, the global nature of the Redux store makes it tricky to handle state for multiple instances of a component simultaneously.

What we want is a way to carve out a section of a store for each instance of a component? As of @angular-redux/store 6.5.0, this exists: it’s called @WithSubStore. Here's what it looks like:

This looks almost identical to the original Redux example above. However, we've added a couple of things. The first is an @Input() called basePath that allows us to declaratively tell NgRedux what part of the store belongs to this instance of the component. We can now update our application template as follows:

The @WithSubStore decorator is affixed to the component itself and tells Angular-Redux to associate a slice of the store with a local reducer. The basePath is determined by calling the method specified in basePathMethodName; this is done so that each instance of the component can use a different slice of the store. Finally, localReducer is simply a reducer whose state manipulation is restricted to that same slice of the store.

When a component has the @WithSubStore decorator, any @select or @dispatch decorators are changed to operate on their slice of the store instead of on the global store. This ensures that selections will be rooted at the given base path, and dispatches will only be delivered to the localReducer for that component instance.

Conclusion

The tab example may not seem complicated enough to justify the introduction of another feature into an already complex framework, but it illustrates the pros and cons of keeping state in the Redux store versus keeping it in the component. Should you always use the @WithSubStore approach? It really boils down to how much granularity you need: in our experience, any component whose logic starts to look like a complex state machine can benefit greatly from this approach.

A lot of the power of Redux comes from the global nature of the store, and the way it forces you to think of your application as a single, large-state machine. However, there is also a lot of value in non-globality, because smaller, well-encapsulated components are easier to re-use, even across different applications. @WithSubStore helps you have both the transparency and debuggability of a global state machine with the re-useability of an encapsulated UI component.