Stores with Dependency Injection in Vue 2 with Inversify
I’m a web developer and by far the best developer experience I ever had is with Vue 2, with Typescript, with decorators and class components.
I find just much more clear, concise and eloquent declaring props and state as class attributes, methods as class methods (duh), and computed properties as getters. Using Vue the typical store choice is Vuex. Pinia is an alternative but Vue 3 is a big no, and Composition API + Vue 2 is an even bigger no. I’m not digging deeper into this, but the dev chapter of my company is seriously considering switching to Angular to avoid using Vue 3. We kinda hate it.
Anyway, Vuex is a nice store, with a big drawback: its complicated, and it has an horrible way to commit actions and mutations (by name, (like php?!))…
The case
While scaffolding a new view, a fairly complex one, in a company project, a very complex one, we had to get smart about the business logic of the view: long story short, the view has a big data table showing different records populated from a backend API, and various filters scattered in split panes, modals and whatnot. It seemed a clear case of moving the logic inside the store:
- the data table watches a getter on the store that returns
Model[]
; - the various filters update the
FilterObject
stored inside the store; - the store updates its internal state calling the API with some filter parameters every time the
FilterObject
is updated.
This way the getter returns always a filtered Model
and everything is totally decoupled, to allow designers to tinker with the UI without making us dev really unhappy moving around events, wrappers and whatsoever.
The store
Having previously tried Inversify as a replacement for http mixins (a rather bad thing), it came to us that it would be much easier to implement and use as a store a singleton service.
The idea is simple: the store is a Typescript class
which extends Vue (so it is technically a Vue component, more on this later). The store is bound to the IOC container in singleton scope. The store is injected inside every component which updates the FilterObject
or displays the Model[]
.
It looks like this:
Glossing over missing type declarations its working are quite simple: we leverage Typescript getters and setters to react to attribute updates: every time a the filter is updated we advertise the filter update event and then call the API (hopefully with a debouncer). The API call will update the items
attribute and advertise the fact that the items may have changed.
Extending Vue (i.e. declaring the store as a Vue component) allows us to leverage Vue events to advertise stuff! Event bus baby!(line 31)
Now we just need to:
- singleton this class with more Inversify
- subscribe to relevant events from the view components.
The container
Now we need to bind our shiny new Store
to the container. Here we encountered some roadblocks, but it turns out it was a bad written guide!
In the main.ts
where we declare and mount the Vue instance we must initialise the dependency container. Not that reflect-metadata
is imported before everything else. Weird errors could be thrown otherwise.
not before building the container:
This functions makes a bit clearer the @inject
syntax found in the Store.ts
. We will anyway see more on that just now. Note that the Store is declared in singleton scope: this means that it will be instantiated just once. Everyone asking for the Store object anywhere in the single page application will get the same instance, allowing us to use it as a store!
The components
Now that we have a IOC container and some injectable stuff fired up and running we can do fancy stuff without anything more than an @inject
decorator.
For example, a component updating the filter could be:
Vue on
and off
methods can be used to hook into the store events. This SearchBox
components can be dropped anywhere on a view and it will always be synchronised with the store singleton.
Note that the store is injected with a decorator, it is possible to manually get it from the container by calling container.get
inside mounted
. I find decorators much more elegant and concise.
On the other side, with some imagination we can image this component dynamically refreshing its contents as the store updates its state:
Even more quick and easy. Just subscribe to the items-updated
event and the component will (not so) magically be synchronised with the search box.
Conclusions
Obviously this is a very simple example, but it should satisfy the need to se a complete example of Inversify DI in actions with prop injection in class components.
The store could contain more logic and even actions (as simple methods), even if we prefer to keep inside it the logic strictly related to items searching and filtering. Adding stuff to it just because it’s easy and it works can cause “big store syndrome”.
There are obviously limitations: for one any persistence must be done by hand. We did not yet encounter this requirement, as this is mostly a runtime support for a view. Also, there being no mutations there is no history, so this is not the thing you are looking for if you need history.