When and why use services or plain functions in Aurelia Store

Hi,

I know there are already some elements of answer in Aurelia store: design questions but I’d like to focus on this point and see if my understanding is correct.

From what I understand, there are two ways to work with the store: do everything in functions in a functional way (and perhaps also in a React/Redux way) or rely on good old services and use the pattern described here to dispatch an action (which must be a function) but still use a service under the hood.

To summarize what I understood:

Services

  • Advantages
    • Good old objects, nice for grouping related behaviours in methods and to avoid repeating arguments since we can use their internal state.
    • Nice if you don’t need something like Aurelia store.
    • You don’t need to know how Aurelia store works while writing a service. You can write them to respect an interface as usual.
    • I feel like it would be easier to rely on services to provide different implementation of a given feature. For instance, in an RSS application, I may want to be able to use different RSS backends (TTRSS, FreshRSS, …). With services, I can create a TTRSSBackend and a FreshRSSBackend that adhere to an interface and then rely no DI to get the proper instance. Something like:
function getBackend(backendName) {
    return Container.instance.get(backendNameToClass[backendName]);
}

function myAction(state) {
    const backend = getBackend(state.selectedBackend)
    backend.myAction().then(updateState)
}
  • Problems
    • Their state must be kept up to date with the store. So we cloud rely on an action to update the state through the method of a service but still inject in the service the state so everything stay up to date. But it feels a bit complicated/strange to me.

Functions

  • Advantages
    • Feels a bit more natural given that actions need to be functions. We also don’t need to get services with advanced usage of the DI.
    • Purely functional, no risk for the store to get out of sync with internal states of services. The state is really in one place.
  • Problems
    • If we are migrating to Aurelia Store we may already have a bunch of working services. I don’t think it will bring much value to discard them. So use them with Aurelia Store can be a good idea.
    • You must embrace a different pattern (but I don’t think it’s a big deal).

So I think that by default we should rely on functions: it seems to be the most common pattern (both in Aurelia Store and other similar stores) and it avoids bringing objects with their own state into the mix. I also think that depending on the use cases, we may also have a mix of functions and services.

Do anyone have something to add or some precisions to make?

2 Likes

I think you summed up all perfectly well with one thing left unclear and that is keeping services in sync. Are you storing data inside props of the service? E.g the rss service would it keep the entries around inside the class in order to bind to it from a consuming View?

Services typically, coming from Angular or WPF consist of two parts. Methods to allow CRUD on data and sideeffects plus storage of data in a single class instance , controlled via DI. Now what I’d do with the store Plugin is to keep the shallow shell of service methods but actually perform action dispatches. And additionally dont keep service state but really just only use a state subscription. You can persist the data inside the service but treat it as immutable objects.

If you could show a concrete example we could elaborate together what the pain points are and what could be a better solution. So a minimal Codesandbox would be perfect

3 Likes

So do dispatch directly within the methods of a service? And not something like (so the service has no idea the store exists):

function fetchUnreadArticlesAction(state) {
    const backend = getBackend(state.selectedBackend)
    backend.myAction().then(articles => dispatch(updateStateWithUnreadArticles, articles))
}

function updateStateWithUnreadArticles(state, articles) {
    const newState = Object.assign({}, state)
    state.unreadArticles = articles
    return newState
}

By single class instance, do you mean an instance of the service or some other class (some kind or store)? I think it’s the first solution but I’d like to be sure.

To give some context: I am building a small RSS app with Aurelia. My goal is to learn and try new things with it (like TypeScript and Aurelia Store). Currently, I have one RSS service. I store some authentication infos in it but I don’t store the articles in it. Currently, they are only used in one component. So it calls getUnread to get unread articles and stores the result for display. If I had multiple component which relied on the list of articles, I’d probably store them in the RSS service to avoid unnecessary requests and inject the instance of the service in all the components.

If you want something smaller, I should be able to work something out by next week-end.

2 Likes

Exactly, this way the Service acts as a kind of Data Access Object (DAO) and you can think of dispatched actions as the Query performed.

Yes by single instance I meant the service class. There is rarely the need for a transient service.

An RSS reader sounds like a fantastic small project to tinker with. So whenever you have time I’d gladly take a look for a review. Just leave some comments around where you feel unsure.

1 Like

Thanks! I now have to actually use Aurelia Store :wink:

1 Like

I am in the process of migrating to Aurelia Store. I have two questions:

  1. Should I keep injecting my service to trigger actions or should I switch everything to actions? For instance, on my home page, I want to get all unread articles. Should I inject the RSS service and use it like this:
activate() {
    this.rssBackend.getUnread();
}

or should I dispatch an action which will use the service:

activate() {
    this.store.dispatch('fetchUnreadArticles');
}

I feel like the second solution would be more appropriate: I only need to depend on the store and nothing else. What do you think? (Full code for this route is here)

  1. Before I used the store, to mark an article as read, I’d use the markAsRead method:
class RssBackend {
   markAsRead(article: Article) {
     return requestApi(article).then(() => article.markAsRead());
   }
}

But with the store, I shouldn’t directly mutate the article, I should copy and then update. So my code looks like:

class RssBackend {
   markAsRead(article: Article) {
     return requestApi(article).then(() => this.store.dispatch('markAsRead', article));
   }
}

and my action:

function markAsRead(sate, article: Article) {
  // Copy State
  newState.rss.unreadArticles[indexUpdatedArticle] = newState.rss.unreadArticles[indexUpdatedArticle].clone();
  newState.rss.unreadArticles[indexUpdatedArticle].markAsRead();
  return newState;
}

I find it a bit weird: I have to fully clone the object and then use a method on it. So my question is: should I drop these methods which mutate the state and use plain object instead? So something like:

function markAsRead(sate, article: Article) {
  // Copy State
  newState.rss.unreadArticles[indexUpdatedArticle] = {...newState.rss.unreadArticles[indexUpdatedArticle]} as Article;
  newState.rss.unreadArticles[indexUpdatedArticle].status = Status.unread;
  return newState;
}

Full codes samples: RSS Service and Action.

1 Like

As always there is no single right direction. Your thinking though sounds good for me. 1. Only depending on store vs services is certainly a benefit. You might need the service if you’re migrating an existing app. But you can always refactor later.

  1. Yep I’d go with object destructuring. Keep in mind that this does no deep copy, which can be ok in several cases.
1 Like

Indeed :wink: But I always find it interesting to have my opinion challenged by people with more experience than me on a subject.

Thanks again for your answers!

1 Like

And thanks for all the examples at https://github.com/zewa666/aurelia-store-examples They are really usefull!

1 Like

I think you’re on a pretty good track so far. I’d recommend, even if you currently arent testing, to take a look how your setup feels for unit tests. That often gives good insights into flexibility of your architecture. E.g having solely the Store injected makes it super easy to mock the dispatch function and only let specific actions pass

2 Likes

Indeed. I tend to leave them out for small apps because it’s easy to test by hand. Will add some though.

2 Likes