This article was first published on Pragmatic Coder by Aurelia core team member, Vildan Softic. If you enjoy this, why not head over there and check out some of his other work. And, if you’re interested in keeping up to date with the latest news related to Aurelia, you can sign up for their official newsletter here.
We can’t ignore the ever-growing importance of changing paradigms, no matter whether we’re talking about social, political or software development matters. Of late, front-end web development has seen an increasing interest in predictable state containers, introduced by concepts such as Flux and made popular by Redux. Simultaneously, the trend towards a more functional style of programming — particularly component composition — has changed the way we think about building applications.
At first glance, neither idea may appear that important or world changing in its own right, but put together they can provide a great developer experience. I’m not going to judge whether this is a better approach compared to well-known concepts such as MVVM and classic services. Rather I’d like to share an approach which helps you to combine both concepts, so as to get the best of both worlds.
This article talks about the theory, actual code and a full example can be found over at GitHub. Sources, including templates, are fully commented to explain design choices and the repo’s README contains a recommended way to review the example. As such we won’t waste time on implementation details, like the use of RxJS, but get straight to understanding the core concepts.
A Modern Development Approach
A modern development approach leverages a single store, which acts as a fundamental basis for your application. The idea is that this store holds all the data that makes up your application. The content of your store is your application’s state — a snapshot of your application’s data at a specific moment in time. In functional terms, if we were to represent our whole application with a single function
renderApp, the state would be the argument we pass in.
function renderApp(state): RenderedApplication
If we only wanted to produce static sites without any interaction, we’d already be good and could stop work here. However, most of today’s apps provide a plethora of interactions. So if the state is a snapshot at a specific point in time, an event can be seen as the trigger that changes our state from current to new. Such a user interaction can be compared to a reducer, which modifies the current state by applying instructions from a certain action.
function userInteraction(oldState, ...actionInstructions): NewState
Modification though, is a dangerous game. If we change the original source, how will we know the difference between the new and old state? As such immutability is a key aspect of modern approaches, as it maintains the original source and creates a modified copy of your new state. So the current state becomes the old state and the interaction creates the next current state.
CURRENT STATE --> USER INTERACTION --> NEW STATE renderApp(currentState) --> userInteraction(currentState, ...) --> renderApp(newState)
Past, current and the future are snapshots of state after a given amount of actions. Keeping this in mind we can move the current state backwards, by reversing actions and traveling back to a previous state.
NEW (aka CURRENT STATE) --> USER INTERACTION * -1 --> CURRENT (aka OLD STATE) renderApp(newState) --> userInteraction(newState, ...) --> renderApp(currentState)
The interesting point here is that the functional call sequence does not change — only their inputs do. As such we can conclude that a state is solely influenced by actions and that given a specific input, we can always expect the same output. This reflects the nature of pure components.
A Single Controlled Store
A single controlled store starts to make sense as, if we can constrain all changes to a single place, we maintain control over the result, thus the rendering of our app. That is our store. Solutions like Redux force you to design and create your application in a rigid manner which might not ultimately fit your design goals.
Another important thing to bear in mind is that while people are reluctant to change behaviors and adapt to new paradigms, this goes double for corporate enterprise. Consequently, applying a fundamentally different development approach to existing software is somewhat of an uphill struggle.
Developers working with Aurelia often have a solid understanding of the MVVM pattern, which most of the time promotes services as a best practice to keep your business logic separated from your UI logic. Combined with Aurelia’s dependency injection we get singleton instance handling actions. Yet the constraint of a store is missing, as a service by itself does not dictate where and how you should access and modify your data. Does the service keep the state? Do you only allow it to be modified using setters and accessed via getters? This flexibility is both a blessing and a curse, as it means that you can build and structure your applications however you want — unless you have neither the time nor the interest to think about it :)
Using Service Methods as Store Actions
Using service methods as store actions is a way to maintain data access through services without having to change your overall existing application architecture. Instead of injecting the service, you inject the store. Instead of accessing service methods, you subscribe to changes of the single state and trigger actions on the store. These then call service methods by themselves, update the state and thus trigger a redraw.
How components interact with the store
Rendering Applications with Components
This is done in Aurelia by using custom elements. Similar to React and other functional reactive programming (FRP) oriented frameworks, this facilitates component composition. Working with a single state will suddenly make you embrace the notion of dumb vs smart components and higher-order components (HOC). Why? Well, let’s start with the HOC. Its sole purpose is to reference and sync the single state and propagate either itself or its actions and partial data to its child components via inputs.
In Aurelia this means you’ll be using a custom element, which injects the store and creates a subscription to its changes (HOC VM example). The state reference is then passed on to smart elements and the partial data to dumb elements, via possible actions (HOC View example).
The difference between a smart and dumb component/element is determined by whether it has knowledge of the store, or is completely isolated from the rest of the app and gets all of its data passed to it via inputs/attributes.
Dumb components are decoupled from their environment and thus can be reused more easily. As a rule of thumb, if you want to create simple presentational components, which only render data they are provided and pass on callbacks to the given actions, then you want to go with dumb components. If a component, on the other hand, is not going to be reused in other places and has more complex UI state to handle, you’ll likely want to use smart components. Keep their count as small as possible though.
We’ve covered quite a lot of ground here. Now I’d recommend taking a look at the sample code on GitHub. If you have questions or comments, I’d love to hear them in the discussion below.