PatrickJS / NG6-starter

:ng: An AngularJS Starter repo for AngularJS + ES6 + Webpack
https://angularclass.github.io/NG6-starter
Apache License 2.0
1.91k stars 1.35k forks source link

Reloading state data #111

Open baj84 opened 8 years ago

baj84 commented 8 years ago

Hey,

What would be the best way to reload the data for a component? We currently have many components on a page and am unsure on the best way to allow the user to go back and fourth on the browser and keeping the state data persistent.

Cheers

fesor commented 8 years ago

store application state somewere in services, get it using resolvers, pass to components via bindings, change state by asking services.

samithaf commented 8 years ago

I am using Angular cache factory within the service layer to solve this issue.

fesor commented 8 years ago

@samithaf could you share with more details?

samithaf commented 8 years ago

Sorry for the late response.

First you can register your cache factory as follows.

.factory('app.core.common.mario.marioCache', $cacheFactory => {
    'ngInject'
    return $cacheFactory('app.core.common.mario.marioCache')
  })

and then you can write a service as follows which will consume the cache factory.

class MarioService {
  constructor ($http, $log, $rootScope, $cacheFactory) {
    this.$http = $http
    this.$log = $log
    this.$rootScope = $rootScope
    this.$cacheFactory = $cacheFactory

    this.model = {}
  }

getDetails(){
// pass the cache factory to http get. So Angular going to cache the data
 return this.$http.get('api/get/queen', { cache: this.$cacheFactory }).then((res) => {})
}

}
MarioService.$inject = ['$http', '$log', '$rootScope', 'app.core.common.mario.marioCache']

export default MarioService
fesor commented 8 years ago

@samithaf well... this doesn't solves any issues. This is just http response caching.

The main point it that all responsibilities on managing app state (not view state) should be in services. Using http cache, object stores or anything else is just implementation details.

samithaf commented 8 years ago

@fesor by setting cache to http object when you request the same URL angular will not going to send another request. The original question is how to prevent data get loading from server again and again isn't it? So by adding cache problem can be solved.

fesor commented 8 years ago

@samithaf nope. Imagine that there is no backend. All state is stored in memory. And if you have many stateful components, you could easily lose control and break state consistency. This is common problem for stateful components.

It's more about where to store state, in components or elsewhere. From where components should get state? Should it require state from services directly or there is a better way?

My answers was that components should be stateless and all required state should be provided from outside via bindings. Components shouldn't change application state directly, instead of it, they should ask service layer.

@baj84 Does that helps?

slurmulon commented 8 years ago

@baj84 if you are struggling with state a lot, I have written an Angular module that will only delegate state updates when necessary (via PubSub) to your contextually relevant entities, giving you more control over when entities are updated. State is stored in Services (chosen because it's a canonical singleton):

https://github.com/slurmulon/ng-current

I have used a similar approach in production - the idea is reacting to changes when necessary instead of relying so heavily on the synchronization of the digest cycle through rapid polling (this is something I have always felt Angular 1.X did wrong, but was right for the time). As soon as we integrated this basic concept, issues such as state drift, missed updates and unwanted state refreshes became a thing of the past as their management became more transparent.

fesor commented 8 years ago

@slurmulon are you receiving state inside components?

slurmulon commented 8 years ago

@fesor Yes, a directive/controller is where you ultimately receive your states. Check out this demo code for an example:

https://github.com/slurmulon/ng-current/tree/master/demo/services (contextual services) https://github.com/slurmulon/ng-current/blob/master/demo/directives.js (components incorporating contextual services)

For a working example, check out http://plnkr.co/edit/XlQ9ho?p=preview (particularly script.js:L248-290, helps to outline the general use case through data). The idea is that you commonly work with a hierarchy of related component entities (User -> Site -> Quote), each of which may be "selected" by a User at a certain time. For example, a User may have multiple Sites (perhaps a clearer term is ConstructionSite for this example, whatever), but you need to track their selected choice so that more detailed information may be displayed later on in some component. If the User switches a Site, you might want any dependents (Quote) to be updated as well.

Note how when you click "Switch Users", the collection of Sites and Quotes represents those only belonging to the contextually relevant User. When you click "Random Site", only the list of Quotes changes, and only if the newly chosen random Site is different from the previous (intentional to show how this system only delegates changes when necessary)

fesor commented 8 years ago

@slurmulon there are much simpler and cleaner solution of your problem:

http://plnkr.co/edit/fJP3akbbngumTOPygBPG?p=preview

In this case I can even drop "Site" and "Quote" services, since it just an objects available by references. This code is super easy to test and maintain.

p.s. Stop using scope, really. And please follow angular-styleguide.

slurmulon commented 8 years ago

I happily accept cleaner solutions and I appreciate your example. However I have worked with many great Angular/SPA engineers for nearly 6 years now who have struggled with handling the various implications of this problem, so this is simply a result of those experiences.

I view that there are a couple undesired complications with your example.

  1. Your code does not allow you to easily abstract out business/model logic for Site or Quote with discrete services as Angular encourages. Cleary you have chosen on this design, but it assumes that you should explicitly access and manage every sub-entity of User, which doesn't encourage clean abstraction or proper encapsulation as far as I see it.
  2. You must manually delegate the updates to other dependent entities wherever they are used, logic which you have chosen to reduce to:

    onRandUserPressed() {
     this.User.all()
       .then(users => randomItem(users))
       .then(user => {
         this.currentUser = user;
         this.sites = user.sites;
       })
       .then(() => this.onRandSitePressed())
    }

    The entire point of this library is to prevent redundant code such as this because it's repetitive, prone to error and exposes complexity where you least want it. This logic must be re-written (or alternatively abstracted as I've done) in every component that involves either selecting or updating a User, Site, or Quote entity, which in the UIs I have worked on often involve many inter-dependent components. In other words, with this design, you must always remember that any time you switch a User, you must also switch a Site. In this particular snippet, I also find it strange that you must chain .then(() => this.onRandSitePressed()) as this functionality should be orthogonal to the functionality of the "Switch User" button - seems to be a mix of concerns.

    These issues will be exacerbated when you need to coordinate the source of your data between cache and the latest API representation. For instance, what if your API supports representing resource entities as either links, partial or full responses? The logic you're suggesting easily breaks down under this common scenario because it strongly depends on a canonical source of data in order for it to stay clean and maintainable. Contrary to your statement, I do not believe that this code is easily maintainable in a large modern application.

If you have encountered complex problems such as these and elegantly solved them in the past, I'd be curious to see how you did so. Thanks for the feedback!

fesor commented 8 years ago

Your code does not allow you to easily abstract out business/model logic for Site or Quote with discrete services as Angular encourages.

I prefer to place such logic in my domain entities. I trying to make services stateless.

The entire point of this library is to prevent redundant code such as this because it's repetitive

In my projects I use ui-router's resolvers to get application state. In that case I would have 3 resolvers. Separate services which know how to get application state and nothing more. So... what's wrong with separation of concerns?

in every component that involves either selecting or updating a User, Site, or Quote entity

Components doesn't know anything about how to get or update entities. They are want state passed in and services to ask for update. Components should be stupid and stateless, and then we won't have problems with it.

I also find it strange that you must chain .then(() => this.onRandSitePressed()) as this functionality should be orthogonal to the functionality of the "Switch User" button - seems to be a mix of concerns.

Well.. this could be solved in other way:

class MyComponent {
    // ...
    get user() { return this._user }
    set user(val) {
        this._user = val;
        this.onRandSitePressed();
    }
}

And since "sites" are references to users, they also should be updated after switching user. Or maybe this is some strange business logic.

Contrary to your statement, I do not believe that this code is easily maintainable in a large modern application.

The main point is to use stupid stateless UI components. They have only one responsibility - representing state. The should not know where this state came from or how to update it. Only service layer know this. (my app component know how to get state just because we don't have router in this example)

I tried solutions like yours year ago or so, and it just doesn't work. It hard to test, it hard to work with. In my case, there was no problems so far.

I will try to update some opensouce app (docker distribution frontend for example) to my approach, maybe this would help.

slurmulon commented 8 years ago

Your code does not allow you to easily abstract out business/model logic for Site or Quote with discrete services as Angular encourages.

I prefer to place such logic in my domain entities. I trying to make services stateless.

But by making your Services "stateless", you have no easy way to share/delegate that state without manually propagating it through a chain of dependencies, scope bindings or verbose state providers. I agree that statelessness should be generally desired, it has many benefits. But ultimately there are many common scenarios where the changes in one component can affect any number of other component states, directly or indirectly. How would you easily orchestrate something like this without some sort of high-level supervisor (I guess I'm thinking of this under the Actor paradigm)? My solution was to look at each Service as a synchronized and canonical source of state for your entities, and this has worked quite well for me. I'm definitely curious if you have avoided issues such as this entirely in the past, but from everything I've learned about state, concurrency, functional parallelism, etc., it seems you would experience some design complications without such entities as the application grows.


The entire point of this library is to prevent redundant code such as this because it's repetitive

In my projects I use ui-router's resolvers to get application state. In that case I would have 3 resolvers. Separate services which know how to get application state and nothing more. So... what's wrong with separation of concerns?

I suppose I just view it as pointless to involve entities such as this - everything has a sense of state regardless if the underlying mechanisms are stateless, even a Restful API, so it feels strange to abstract that out in such a decoupled manner. Another problem with this is that anywhere a Service is used, I must be sure to resolve against that state's route with the latest and greatest, which seems cumbersome and repetitive to me (but admittedly free, you won't run into walls this way). As I understand it, if you use your Service in 10 different components, which each have their own state, you would have to write 10 resolve blocks with perhaps even more state provider statements. I have worked on applications with nearly 100 directives/components, each of which may involve multiple Services, so that could easily add up to hundreds of identical resolve/bind/scope/provider statements being made. Using angular-ui-router on large code base just seemed to complicate my state management scenario more than resolve it (the entire team felt this way), particularly because the applications I've worked on had a lot of complicated states which could not always be represented via URLs, forcing me to partially diverge from the framework and thus a canonical source of state (sad times). I also had problems with making certain data accessible in nested components as your state always stems from a high-level component that's mapped to a route. If you're using isolate scopes, which is common, the verbosity and duplication is incredibly frustrating.


in every component that involves either selecting or updating a User, Site, or Quote entity

Components doesn't know anything about how to get or update entities. They are want state passed in and services to ask for update.

Nothing in my code needs to know how it acquires or performs updates on entities, it simply knows that it

  1. depends on a certain entity because it uses it, and
  2. must provide functionality for allowing the user to be switched. It simply calls Contexts.select('user') with the new expected user, which only this component should know how to determine otherwise the logic will get pushed into a shared Service where all components have access to it yet only one actually cares about it. It only propagates the data and doesn't concern itself with anything beyond that, including how the entity or any of its related entities should be updated.

I also find it strange that you must chain .then(() => this.onRandSitePressed()) as this functionality should be orthogonal to the functionality of the "Switch User" button - seems to be a mix of concerns.

Well.. this could be solved in other way:

class MyComponent {
    // ...
    get user() { return this._user }
    set user(val) {
        this._user = val;
        this.onRandSitePressed();
    }
}

And since "sites" are references to users, they also should be updated after switching user. Or maybe this is some strange business logic.

That's the problem I'm trying to solve transparently with ng-current (e.g. sites would also need to be updated after switching user, this is a super common scenario for me). I still feel that there is too much manual work involved with your solution that is prone to mistake, mostly because it doesn't scale well to a larger entity hierarchy. Why wouldn't you prefer something like:

class BtnNextUser
  constructor(Contexts, User) {
    'ngInject'

    this.contexts = Contexts
    this.resource = User

    User.use('current', (user) => this.user = user)
  }

  next() {
    return this.resource.all((users) => {
      // we just want the current user, it can come from anywhere. our component doesn't care.
      const current = this.contexts.current('user') 

      // first user that isn't the current one
      const next = users.find((user) => user.id !== current.id) 

      // select next user in the list, transparently delegating update to all 
      // dependent Services and components, including this one (re-binds `this.user`)
      if (next) {
        this.contexts.select('user', next)
      }
    })
  }

In this way, you don't have to needlessly invoke this.onRandSitePressed();, it only ever gets called when it needs to by the user, and this guarantees that this component will always be working with the application's relevant user selection and never anything else.


Contrary to your statement, I do not believe that this code is easily maintainable in a large modern application.

The main point is to use stupid stateless UI components. They have only one responsibility - representing state. The should not know where this state came from or how to update it. Only service layer know this. (my app component know how to get state just because we don't have router in this example)

I couldn't agree more, stupid stateless UI components should be preferred. ng-current doesn't break that paradigm that at all since the component utilizing it doesn't have to know/care where the state came from, it just needs to concern itself with the semantic of the state (i.e, what does this state mean to me?), which is an appropriate consideration in my eyes. All a component needs to know is what entity it depends on and what the context of that dependency is, which is exactly what ng-current encourages (just inject User if you want it, then call User.all(), User.byId(), User.current(), whatever makes sense for the context of the component). It's not much different than pushing this logic up into the router and having a state provider determine this context instead - the latter just involves more user code. I believe your approach provides the same behavior, however it seems to involve more explicit state setup, repeated bindings and a strong dependency on the URL, which is what I want to avoid. How have you addressed managing the state of components that don't fit into the URL scheme?


I tried solutions like yours year ago or so, and it just doesn't work. It hard to test, it hard to work with. In my case, there was no problems so far.

I will try to update some opensouce app (docker distribution frontend for example) to my approach, maybe this would help.

Interestingly I have experienced the opposite, testing became sane, incredibly predictable and easy to stub once we integrated these concepts. You have complete control over the state because you can easily mock out the implementation of your Service, no route or state configurations are involved whatsoever. You can also easily determine how any state was reached because each transition correlates with a discrete testable unit of business logic. Worst comes to worst, everything is stored on $rootScope.current and all states are achieved through $broadcast and $on, so there should be no surprises here.

What tools/approaches did you use in the past that were similar to ng-current? I'm also curious how you cleanly coordinated cache and API representations with your approach.

Thanks for putting together more resources, really interested to take a look.