Akita: [Feature Request] IOC Container for Stores and @EntityStore Annotation

Created on 8 Aug 2018  Â·  22Comments  Â·  Source: datorama/akita

[X ] Feature request

Current

import { Todo } from './todo.model';
import { EntityState, EntityStore } from '@datorama/akita';

export interface State extends EntityState<Todo> {}

@Injectable({
  providedIn: 'root'
})
export class TodosStore extends EntityStore<State, Todo> {
  constructor() {
    super(initialState);
  }
}

Future

Create Store

import { Todo } from './todo.model';
import { EntityStore } from '@datorama/akita';

@EntityStore(Todo)
export class TodosStore {
    //Custom store methods
    //The store could wrap the service instead of a service wrapping the store
}

Retrieve Store

//Akita IOC Container provides the stores
let todosStore:TodosStore = AkitaContainer.get(TodosStore); 
//If needed initialize state
todosStore.initializeState(intialState);

Initialize State

//If needed initialize state
AkitaContainer.get(TodosStore).initializeState(intialState);

Query

;
let todos$: Observable<Todo[]> = AkitaContainer.get(TodosStore).selectAll();
let todos$: Observable<Todo[]> = AkitaContainer.get(TodosStore).select(filter:Function);
let todo: Observable<Todo> = AkitaContainer.get(TodosStore).select(id:ID);

Thoughts?

Nice to have question

All 22 comments

Thanks for the suggestion. My thoughts:

  1. You are bypassing the Angular DI, why?
  2. If I use a decorator (@EntityStore(Todo)), I will not get typescript intellisense.

Thanks for this awesome library! I've been looking for something that eliminates the boilerplate associated with Ngxs / Ngrx and this comes very close ... maybe as close as possible ... just wanted to see if you think it could be collapsed even further?

You are bypassing the Angular DI, why?

I suggested bypassing because I thought it might be simpler to have the decorator work with a minimal DI container that just houses a static API for accessing stores. This would also make Akita useful outside of the Angular context. We could for example use it in a generic Google Workbox PWA application or Electron implementation or even (Yikes) React applications.

If I use a decorator (@EntityStore(Todo)), I will not get typescript intellisense.

I'm hoping it's so simple we don't even really need it. For example .... right off the top of my head ...

@Injectable({
  providedIn: 'root'
})
@EntityStore(Todo)
class TodoStore {
    //Inject the state
    constructor( private state:State) {
        this.state = state;
    }
    getUrgentTodos() {
        return state.filter( todo => todo.urgent == true);
    }
}

So that's my 50K feet take on it ... It seems like it could be this simple, but you definitely have more experience than I do.

Started investigation of some of the implementation ideas on SO

Update

Probably talking WAY TO MUCH ... but my gut tells me that we should have to specify:
1) That we want a store (The @EntityStore annotation)
2) What type we want it to store (Todo instances for example)

And that's it. Gonna zip it and start reading through the code base now. I'll probably be enlightened.

Noticing some overlap with @fireflysemantics/is for the utilities. I plan on adding isPlainObject() etc ... in case you want to delegate the utility functions.

I wonder if a minimal store could just annotate the model directly? So:

@EntityStore()
class Todo {

}

That would create an EntityStore for all Todo instances. It would have a non configurable API ... but that might be enough in 99% of of all usage scenarios ...Could perhaps also add @Query decorated methods directly to the model ... not super pure ... but desperate times desperate measures :)

If you write code like this:

@EntityStore()
class Todo {

}

You will not get typescript intellisense for the built-in methods, like add(), set(), update(), etc. Check it yourself.

I'm assuming the lack of intellisense could that could affect the ability to refactor the code base as well, which is an obvious minus. Perhaps both approaches are applicable. For example personally I'm assuming that most of the time (99.99%) I'm doing very simple crud / query operations like:

Initialize the Store

let todos:Todo[]=todoService.get(todoServiceUrlRoot);
AkitaStoreCache.get(Todo).add(todos); 

Other CRUD / Query Operations

AkitaStoreCache.get(Todo).delete(todos); 
AkitaStoreCache.get(Todo).update(todos);
let todo:Observable<Todo> = AkitaStoreCache.get(Todo).find(id); 

So if that's all I'm ever really doing and this code is wrapped in a service that offers intellisense to the rest of the application then I can probably live without the intellisense. If I'm doing it repeatedly or very heavily and would like the intellisense as a productivity tool then I'm assuming I could cast:

let todoStore:EntityStoreType = <EntityStoreType> AkitaStoreCache.get(Todo);

WDYT? Perhaps the current approach is better. Just throwing ideas out there .... Still reading through the code base ... really nice work.

Thanks. As Akita doesn't couple to Angular, your examples could work great with React for instance. I will work on React integration shortly and will take your suggestion into account.

Meantime, with Angular, it's better to stick with the natural flow - Dependency Injection.

Cool! Let me know if there's anything I can do to help. I'm a big fan of this project (And all your great medium articles in general), so if there's anything I can do just let me know.

One part that I thought would be problematic is if we did:

@EntityStore()
class Todo {

}

And the stores are keyed by the Todo type, then the store does not actually exist in the Angular DI container, but I suppose there's probably some sort of injection token type approach that could be used so that a client could still do:

    constructor(store:TodoStore) {}

And have it injected ... but personally I have not figured this out part out yet. Another thought I had for intellisense is that it can be enabled by creating a type for a store via an @types like approach that is used to provide intellisense for non typescript libraries .. if necessary.

Thanks! Nice to hear.

Just a minor heads up ... the link to the docs in this article is broken:

https://netbasal.gitbook.io/akita/~/edit/primary/core-concepts/store

Fixed, thanks.

Great. Another idea for code generation. What if the dev did this:

1) Create a models folder
2) Create create the models within the folder
3) Decorate the models that have corresponding stores with @AkitaStore
4) Run akita generate:store

Step 4 would generate the store code. So for example if we had a model with:

@AkitaEntity
class Todo {
}

The Akita CLI would read the AkitaEntity on the model and generate stores/TodoStore containing:

import { Todo } from '../models/todo.model';
import { EntityState, EntityStore } from '@datorama/akita';

export interface State extends EntityState<Todo> {}

@Injectable({
  providedIn: 'root'
})
export class TodosStore extends EntityStore<State, Todo> {
  constructor() {
    super(initialState);
  }
}

Found some related information on parsing the decorators from the Typescript classes here.

But you don't think it's "violating" the purpose of the CLI? It should save you creating the files by yourself.

Well one way of looking at it is that the CLI could be used to generate model files (And forgive me if any of this already exists. I have not looked at the code for the CLI yet, just noticed that there was one). So suppose we did:

akita generate model Todo

Which would generate models/todo.model.ts and stores/todo.store.ts correspondingly.

On the other hand suppose someone already has a models folder with a bunch of models. They cannot run the above command(Or maybe they could but it might generate stores for all the models then ...), but they could use the @AkitaEntity market annotation / decorator to annotate their models. The Akita CLI would then read the models folder and generate stores for the models marked with the annotation. Thoughts?

I can think of something more powerful. Auto-migrating from ngrx to Akita 😃. Think about this one.

EmmmHmmm ... I just started looking at the CLI code ... does it already do this stuff? It looks like it's off to a really good start!

No, it doesn't. I'm suggesting this as a new feature.

OH - OK - Very cool! I just scanned the code and it almost looked as if all the pieces were there already. I'm so impressed!

Just curious about the use of State. In the examples like this:

export interface State extends EntityState<CartItem> {}
​
@Injectable({
  providedIn: 'root'
})
@StoreConfig({ name: 'todos', idKey: 'productId' })
export class CartStore extends EntityStore<State, CartItem> {
  constructor() {
    super();
  }
}

It looks like we create both the State interface and the store with the model class. Could the State interface be an internal implementation detail of the store or do we loose intellisense if that's the case?

Also - very minor - I pulled the above code snippet from this page:

https://netbasal.gitbook.io/akita/entity-store/entity-store/entity-id

The @StoreConfig({ name: 'todos', idKey: 'productId' }) has todos in it ... Should it perhaps be something more related to CartItems?

Yes, you will lose intellisense.

It's just a dummy example. I will change it to todoId, thanks!

Just noticed this as well (Hope it's ok that I just mention things here as I find them ... if not just throw a shoe at me) from the second akita article:

Returning to our example, we only initialize a server request if the store’s state is pristine, otherwise returning an observable that next() once and complete.

I think the sample code may be a bit out of sync with the explanation ...

I will check it. There are people that subscribers to this repo and this may bother them. I'm closing this issue, for any further questions feel free to send me to my private email - netanel.[email protected].
Thanks!!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

amr-alamir picture amr-alamir  Â·  6Comments

stherrienaspnet picture stherrienaspnet  Â·  3Comments

hoisel picture hoisel  Â·  5Comments

DanielNetzer picture DanielNetzer  Â·  5Comments

brgrz picture brgrz  Â·  4Comments