Skip to content

Instantly share code, notes, and snippets.

@gilbox
Last active September 1, 2015 21:49
Show Gist options
  • Save gilbox/12095aa0bc6822736f8f to your computer and use it in GitHub Desktop.
Save gilbox/12095aa0bc6822736f8f to your computer and use it in GitHub Desktop.
ui-router VS flux+vanillaJS
//
// ui-router
//
$stateProvider.state('person', {
url: '/person/{personId}',
resolve: ($stateParams, AppObject) => {
return AppObject.getPerson( $stateParams.personId )
},
controller() { /* ... */ }
});
$stateProvider.state('person.pet', {
url: '/pet/{petId}',
resolve: {
pet: ($stateParams, person) => {
return person.getPet( $stateParams.petId);
}
},
controller() { /* ... */ }
});
$stateProvider.state('person.pet.tricks', {
url: '/tricks',
resolve: {
tricks: (pet) => {
return pet.getTricks();
}
},
controller() { /* ... */ }
})
//
// flux
//
var dispatch = dispatcher.dispatch;
actionCreator = {
person(route) {
return AppObject.getPerson(route.params.personId)
.then( (person) => {
dispatch('load:person', {person} );
});
},
pet(route) {
return this.person(route).then( (person) => {
person.getPet(route.params.petId).then((pet) => {
dispatch('load:pet', {pet});
})
})
},
tricks(route) {
return this.pet(route).then( (pet) => {
pet.getTricks().then( (tricks) => {
dispatch('load:tricks', {tricks})
})
})
}
};
//
// flux with async/await
//
var dispatch = dispatcher.dispatch;
actionCreator = {
async person(route) {
var person = await AppObject.getPerson(route.params.personId);
dispatch('load:person', {person} );
return person;
},
async pet(route) {
var pet = await this.person(route).getPet(route.params.petId);
dispatch('load:pet', {pet});
return pet;
},
async tricks(route) {
var tricks = await this.pet(route).getTricks();
dispatch('load:tricks', {tricks});
return tricks;
}
};
@ProLoser
Copy link

ProLoser commented Feb 6, 2015

Okay, here's the bottom line:

Dispatchers are useless in angularjs because the live view binding voids the need for them. If you create an object like return new Person($stateParams.personId) and the constructor immediately calls this.load() then all of a sudden my code becomes parallel. The controllers will still just do $scope.person = person and when this.load() is done, angular will automatically update the views to reflect the changes.

Look, I'm not saying flux (or react) is bad. I've actually ported over an app or two to react and have been looking at their roadmap quite a bit. I'm just saying that in an AngularJS environment, flux is completely useless.

I've done exactly what you're doing in AngularJS before and it's a huge pain in the ass. I've also discussed with people about the fact that other libs (in flux / react) act as singletons and almost everything becomes asynchronous.

Have you considered what happens with all the listeners? You now have to bind to every single event in every single controller. And you have to make sure those listeners are unregistered when the controllers are destroyed. Your efforts to use event dispatchers is essentially just a degree of obfuscation on top of angular's watches. When you put data on a scope, that's like binding a listener. When the scope is destroyed, that's like unbinding.

I built a 'SessionManager' class that did almost everything both you and flux proposed, and it was chaos and a huge PITA. For angular, it's just not necessary. For React, it makes more sense (perhaps).

Take a look at relay. It's a system of batching up the queries (data needed to render) and automatically 1: caching, 2: batching, 3: relating the data. If you query a pet, all the parent objects are queries too. You actually HAVE to define the relational tree.

If you don't care about rendering everything in the relational tree, then simply don't use relationships in your classes/objects.

The primary purpose I'm illustrating in the project is to bundle your business logic into classes and have your state manager manage them for you. The state manager is in charge of loading data, and the controller does nothing more than placing it onto the view, and holding some negligible view-only data. There is no use of scope inheritence across controllers.

@ProLoser
Copy link

ProLoser commented Feb 6, 2015

This would essentially be what you wanted, without any dispatchers or subscribing necessary. The problem with using dispatchers or other people's code is you must always hook into the .then() or .on() but in angular since the view is always up-to-date you can simply place an object onto the view and keep it up to date (by preserving references). That's why ngResource allows you to work synchronously.

class Person
  constructor: (@id) ->
    if @id
      @load()
  load: ->
    @loading = true
    @query 'getPerson', {id:@id}, (response) =>
      angular.extend @, response
      @loading = false

@ProLoser
Copy link

ProLoser commented Feb 6, 2015

Btw, this is how you probably should have done searching:

$stateProvider.state 'search',
  url: '/search',
  view: '<h1>Search</h1><input ng-model="query"/><a ng-click="go()">Go</a><ui-view />'
  controller: ($scope, $state) ->
    $scope.go = ->
      $scope.loading = true # keeps view-state out of business logic
      # or $scope.$root.loading = true
      # or $scope.$broadcast('loading')
      $state.go('search.results', { query: $scope.query })
$stateProvider.state 'search.results',
  url: '/:query',
  resolve:
    results: ($stateParams, api) ->
      return api.search($stateParams.query)
  view: '<h1>Results:</h1><ul><li ng-repeat="result in results">...</li>'
  controller: ($scope, results) ->
    $scope.loading = false # or variants
    $scope.results = results

Note how there's a clear separation between view state and business logic data? Note how changing states is all that's necessary to both retrieve new data, fire the loading flag, and reload the new results? See how tiny our controllers are and the complete lack of necessity to handle subscribing and unsubscribing? See how most of our logic is extremely human-readable loading = true/false, go to results

If you really wanted to do memoization of searches, you could do this inside of api, however you'd be given your same query, and refreshing vs navigating is handled identically, and there's never a concern with 'are resources loaded out of order', or other such nonsense.

@gilbox
Copy link
Author

gilbox commented Feb 6, 2015

If you wanted, you could just use the ID's to get the data in parallel, however this design pattern is actually being adopted by the Facebook team using the new Relay structure

Right, the idea with relay is that with graph sql, multiple queries are combined into one query, not sent in parallel.

I don't get the purpose of tying the routing into your actions or stores, and I don't believe that's outlined anywhere in the information about flux.

Pretty much everything is decoupled in this example, I'm not sure what you mean by this.

Have you considered what happens with all the listeners? You now have to bind to every single event in every single controller. And you have to make sure those listeners are unregistered when the controllers are destroyed. Your efforts to use event dispatchers is essentially just a degree of obfuscation on top of angular's watches. When you put data on a scope, that's like binding a listener. When the scope is destroyed, that's like unbinding.... in angular since the view is always up-to-date you can simply place an object onto the view and keep it up to date (by preserving references)

I agree with this, and it's one of my favorite things about your Angular ORM presentation at ng-conf. Unlike Facebook's initial Flux architecture, my flux implementation doesn't use event listeners to keep the view updated.

Dispatchers are useless in angularjs because the live view binding voids the need for them.

I think that dispatchers are useful in addition to the live bindings because they decouple your code and facilitate unidirectional flow.

I've also discussed with people about the fact that other libs (in flux / react) act as singletons and almost everything becomes asynchronous.

The beauty of flux is how it manages asynchronous flow. All of the business logic becomes very easy to test because async operations are completely removed from the Stores. The views are also very easy to test because they become dumb views that just bind to the data. The only real challenge is testing the action creators because that's where all the async stuff happens.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment