Aurelia store: design questions

Hi There,

I’m learning about the new aurelia-store as a consideration for use for a new project having little prior background knowledge regarding other store solutions. I’m a backend developer in nature who does occasional frontend. I’m having trouble understanding some of the concepts and design choices regarding the aurelia-store and store solutions in general and would love to get some feedback on my own ignorance and/or maybe even start a discussion or guides towards some alternatives.

Primarily I’m having trouble understanding why a single global state and why it is implemented in the way that it is. A general trend on the web is to move towards microservices where each microservice owns its data and thereby state. Mutations are performed based on events and actions.

For example: The .NET Orleans framework support EventSourcing however this is an implementation detail of the microservice. If the service decides to use EventSourcing, Its actions can raise events that will mutate its state and it is able to replay those events and expose them to the outside world in whatever way fitting.

Within the rest of this post I look at a well done microservice architecture as the counterpart to using aurelia-store.

Why there is only one

With aurelia-store, I’m encouraged to register the store before startup with an initial state as a global store. No matter how many modules my app may be having, all of them have unrestricted access to all data with the ability to mutate in any way, shape or form (immutable or not). Although I’m somewhat able to trace who mutated what, it still seems like a mess to me.

The docs state an advantage:

here is only one source of truth for your data and all modifications happen in a predictable way

That may be true if you’re a single developer on a project and you’re somehow immune to introducing bugs, otherwise you’re bound to accidentally mutate previous state sooner or later, but to a more important note: This is not predictable at all, If I’m Service A and I owe some data, if that data is shared globally, anyone can touch it really… Possibly by accident with aurelia’s two-way databinding being an obvious candidate for trouble.

Overcomplicated actions

The docs state:

Next, we need to register the created action with the store. That is done by calling the store’s registerAction method. By doing so we can provide a name which will be used for all kinds of error-handlers, logs, and even Redux DevTools. As a second argument, we pass the action itself.

This seems counter to aurelia’s design philosophy. When calling dispatch with an action (function, not name), that implicitly provides the name of the function as well whatever else I’d like to log (like the module that dispatched that action), what is the added benefit of having to register functions? I know when an action has changed by simply comparing the reference. Calling functions by name seems one use case however that would not be a good reason to force all functions to be registered. In a test project on my end I’m having my actions scoped to a module- then I’m having to run a registerActions method from somewhere within my apps bootstrapping logic to register each modules set of known actions (of course hoping to not forget to register an action somewhere). Finally and to be on the safe side, I should also unregister actions… Why? When they are out of scope or inaccessible for me, that should be it right? What am I overlooking?

Services and actions or actions and services or actions

If I accept the above for my own ignorance of not realizing the full potential with this architecture then I’m still confused on how to use it. I have my ideas however the docs and samples seem to counter those.

To portray a scenario: I have an API who’s result should be written in the store so that the app can react on it. Traditionally I would have a service that encapsulates that API call and either or both returns the value and/or dispatches an event using some mechanism like the event-aggregator or RX to which other components then could listen and react. With the Aurelia-store I would do pretty much the same except for rather than returning/publishing an event, I would dispatch an action on the store from within the service/caller with the mutation returned as a response from the API call.

In the github repo called aurelia-store-examples there is the Services demo of how to combine services and store actions (where it has a remark stating that Services are ‘Old school’ :frowning: ). Within this demo, actions are used to trigger the http call through a service (Which smells like a bug since the service is potentially pulled in from DI before the app has a chance to boot). This of course flips things around, now actions are responsible for much more than updating the store. Actions, who by nature are just functions with no real lifecycle on their own. This feels like an anti-pattern to me but perhaps I’m missing things again.

Time travel

One of the advantages of using a state management solution like this that keeps popping up is time travel (being able to rollback events). Aurelia-store does a nice job in providing us with the ability to look back in time. It would just make much more sense if services owned their own stores, that way we could opt-in to this feature per service depending on needs. Now we basically cant use it in production because eventually history will overflow.

Wrapup

I like to go out of my way of getting to understand the next ‘hot’ thing- even though its occasionally a trap. Central state management has its benefits but we’ve discovered those decades ago, that’s why we’re still using N-tier today. Apart from some develop convenience for having to do less in order to support time travel, I fail to see the benefit of having a central store- except for some edge cases. I’d like to dive a bit deeper into this and learn more about what I’m missing or your opinion.

I’m also considering launching an opinionated aurelia setup where I explore the ‘Old school’ approach to setting up a project considering interdependencies and even time travel. If you’re interested in that, please comment below.

Thanks for your time!

6 Likes

Wow I’m super sorry for having missed this topic. I’m a bit stuffed with work atm but I’ll make sure to answer your questions as good as I can soon.

1 Like

What’s regarding patterns recommended for integration of Aurelia-Store and Aurelia-Validation?

Thank you

DISCLAIMER: this is not the official, nor recommended, way to use Aurelia-store. It happens to be my way and it has served me well for a relatively big real-time application.

  1. I have 1 store and that’s it; state are - in theory - immutable (I use immer: https://hackernoon.com/introducing-immer-immutability-the-easy-way-9d73d8f71cb3)
  2. I still have services for everything I do
  3. The services have an always up-to-date copy of state using @connectTo
  4. The services have their own “temporary” observable variables for form inputs; no form inputs are bound - ever - directly to state.
  5. The services have actions that modify data in the store
  6. The services have their own traditional methods to query from / commit changes to the API, and in the API callbacks, invoke actions to commit latest state to the store

Additionally, since I’m using feathersjs, almost no actions of mine are invoked directly (except for login / logout and initial data loading). My services methods commit changes to feathersjs API and that’s it. Meanwhile, I have event listeners for the “created, deleted, patched, updated” events from feathersjs that would actually invoke the actions to commit data to my store.

2 Likes

Primarily I’m having trouble understanding why a single global state

A single store approach is what currently most of the other well-known libraries proclaim the status-quo. Be it VueX, Redux or whatsoever. The initial idea of working with multiple stores, as introduced by the Flux pattern, was as an example pretty much silenced by Redux.

A general trend on the web is to move towards microservices where each microservice owns its data and thereby state.

That’s true, yet it has little to do with frontend state management. If you’re in the search for a system to cope with large-scaled microservice or lets better say micro-frontend architecture than clearly, things like GraphQL are maybe of more interest for you. Yet keep in mind that also GraphQL actually solves a different thing. The idea of frontend state management is first and foremost to have predictable UI state handling. Everything else is just a by-product.

Why there is only one
With aurelia-store, I’m encouraged to register the store before startup with an initial state as a global store.

This one’s simple. Because it turned out to be the best-practice that stemmed over the years. Flux is pretty much dead and the reason to move to one Store is exactly that. One place to have all your data, one place that can be modified. No dependencies between multiple stores, easy extension via Middlewares, since they only work on one object.

otherwise you’re bound to accidentally mutate previous state sooner or later, but to a more important note:

Now that is actually an issue, and clearly the most prominent one in the whole JS ecosystem. The trouble here is not a framework or library, but more the issue that JavaScript as a language itself, has very weak tools to guarantee immutability. Besides const, Object.assign/Object destructuring you’re pretty much on your own. Take languages like Elm or PureScript as a contrary and you’ll see that most of the issues there are solved on the language level.
There are ways to cope with complexity though, and that is libraries like Immer, as mentioned by @khuongduybui. If you can’t control mutation, either by libraries, helper functions or last but not least by rigid discipline, then pretty much every paradigm will fail.

This is not predictable at all, If I’m Service A and I owe some data, if that data is shared globally, anyone can touch it really…

Not with the idea of actions. If you neglect the issue of wrong mutations, the only way to modify the global state is by dispatching actions. The benefit here is that no matter who wants to initiate a change, is able to do so, yet the only way is through dispatching. And this makes it predictable. There no x service competing over which one should store which part and cross-dependencies get eliminated. All of that guaranteed by a single dispatch queue, which again can be manipulated on a high level via middlewares.

what is the added benefit of having to register functions?

This one’s hard to boil down to a concrete point. I could go on with things like having the name for a function is beneficial for e.g having the steps properly named in redux devtools. Not being able to infer the function name automatically, because of how various bundlers work or whatsoever. I talked about this with quite a lot of other devs and for me, it’s generally a safety thing. What I’ve seen too often in Redux land was that users dispatched whatever functions and created troubles. Especially in dynamic scenarios, where the actual action to be dispatched is constructed on the fly, it’s super easy to introduce duplicates which do vary only on a detail. Or that things that aren’t meant to act as actions (ding-dong mutation) do get dispatched.
Put straight, I see the registration as the last line of defense and moment where you really have to think what you want to throw at your store.

Seing how many people seem to be offended by this approach, I’m definitely thinking about an alternative registration-free workflow.

Services and actions or actions and services or actions

I think this is the topic where worlds may collide. As I see it there is no right or wrong. I enjoy Service-oriented architectures too. I’ve just found over time, that if services have too large inter-dependencies, things get out of hand. Together with two-way-binding, this can easily introduce trouble.
Event dispatching, on the other hand, is great for inter-component-communication but lacks the issue of traceability/debuggability. It’s really crazy hard sometimes to replicate issue which happens because of event timing issues :wink:
By saying services are old-school in no way I meant that in an offensive way, but instead just meant to old well-known thingy.

As for the examples provided. They essentially show different ways of using the Store. Not saying all of them are perfectly architectured, which is definitely welcome to be contributed back by the community. The idea though is to show ways how you might use the Store in certain ways. As with most things in Aurelia, we won’t dictate how you have to do things, well except for Action registration that is right? :wink:

It would just make much more sense if services owned their own store

So this is another thing with being flexible. The JS community in large thinks it’s better to have one Store, mainly driven by the concepts Redux presented. Yet looking at how Aurelia-Store is implemented internally, by being a simple class, nothing really hinders you in creating multiple stores. There might be really good reasons for that, I can’t nor want to think of yours, but for the sake of all good go ahead and create two or even more stores. Frankly I’m working on a large application where we use a second store for IPC log informations as we didn’t want to pollute the rest of our frontend with things solely consumed from a single log panel :wink:

Wrapup

I think it’s not in the interest of anybody to convince you or anybody to use a single state management library. On the contrary, I’d personally say as long as you’re good with simple services, stay with them. If communication with various microservices / microui’s is your case take a look at GraphQL. It just happened that I’ve built the plugin out of a need for it because my apps and the ones of other devs just got out of hand. The thing that matters most for me is the reduction of inter-dependencies and a single spot where I can squeeze everything through a well-predictable queue, pre- and suffixed with middlewares for global control and easy integration with RxJS, as that library clearly removes the need for tons of own cazy concepts. But then again these are mainly my thoughts and the library is now owned by Aurelia itself, so if more people startup the conversation we might as well move it into another direction.

Thank you for your questions, as they clearly show you invested quite some time to think through the plugins architecture. As you said it might really not be the right thing for you but that’s ok, maybe it just gave you the extra kick to come up with something even better. I’m definitely looking forward to seeing what you’re coming up with.

9 Likes

Great answer!

In cases like these where I’ve not followed the development of the trend, its often hard to grasp how things ended up the way they are (with hype being a major irrational driver sometimes). I now have a much better understanding in why certain choices were made.

In a next project I have the requirement to develop an undo/redo system which is scoped to certain sections on the page, e.g. a product listing editor where you can make updates and revert those updates. Having a dedicated store with history tracking for that particular product could be a beneficial architecture. On commit the product listing can be materialized and submitted to a global store.

I’ll give aurelia-store a shot for the above and I think I’ll wrap my actions in services as @khuongduybui suggested.

Thanks again for taking the time to clear things up for me, hopefully I’ll be able to contribute something back as a result of my upcoming adventure with the aurelia-store.

3 Likes