We can't ignore the ever growing importance of changing paradigms. No matter whether we talk about social, political or software development matters. Web frontend development has seen an increasing interest in predictable state containers, introduced by concepts like Flux and made popular by Redux. On par with this, the trend to a more functional style, particularly component composition, has changed the way how we think of building applications nowadays. Each idea for itself may not be that important or world changing at the first look, but put together they can provide a great developer experience. I'm not to judge whether it's 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 worlds in order to get the best of both.
This article talks about the concepts, 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 will propose an file order in which you should review. As such we won't waste time on implementation details, like the use of RxJS here, but get straight to understanding the concept.
A modern development approach
... leverages a single store, which acts as a fundamental basis. The idea is that it holds all data, that makes up your application. The content of your store is your application's state. If you will, the app state is a snapshot of data at a specific moment in time. In functional terms, if we would represent our whole application with a single function
renderApp, the state would be the argument we pass in.
function renderApp(state): RenderedApplication
If we would produce only static sites, without any interaction, we'd already be good and could stop work here. Most of nowadays apps though, provide a plethora of interactions. So if the state is a snapshot at a specific time, an event can be seen as the trigger that changes our state from current to new. Such a user interaction, therefore, 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 world. If we change the original source, how would 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 a current becomes the old state and the interaction is creating the next current state.
CURRENT STATE --> USER INTERACTION --> NEW STATE renderApp(currentState) --> userInteraction(currentState, ...) --> renderApp(newState)
Past, current and the future are thus snapshots of state after a given amount of actions. Keeping this in mind we can change the current state also in the backward direction, by reversing actions and thus 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 aspect is that the functional call sequence does not change, but only their inputs do.
As such we can conclude that a state is solely influenced by actions. Therefore, given a specific input, we can always expect the same output, which reflects the idea of pure components.
A single controlled store
... thus starts to make sense, since 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. Now solutions like Redux force you to design and create your application in a given rigid manner. This might not ultimately fit your idea of how you'd like to design your application. Another important thing is that if people are feeling uncomfortable to change behaviors and adapt to new paradigms, corporate enterprise does so even less. As such applying a fundamentally different approach to an existing software, might not work in an all-in style.
Developers working with Aurelia often have a solid understanding of the MVVM pattern, which most of the times 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 to modify it via setters and access it via getters? This openness is a blessing and a curse at the same time, as it permits to build your applications structure the way you want, except if you really don't have the time nor the interest in thinking about it :)
Using service methods as store actions
... is thus a way to maintain data access through services and not having to change our 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, which call service methods by themselves and update the state and therefore trigger a redraw.
How components interact with the store
Rendering applications with components
... is done in Aurelia by using custom elements. Similar to React and other functional reactive programming (FRP) oriented frameworks, it allows doing component composition in an easy manner. 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 gonna be using a custom element, which injects the store and creates a subscription to its changes (HOC VM example). The state reference then is passed on to smart elements and the partial data, along possible actions to dumb elements (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 data passed on via inputs/attributes. Later ones 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 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.
This is quite a lot information for the beginning. I'd recommend now taking a look at the mentioned example at GitHub. If you have questions, feel free to post your comment below.