Skip to content
This repository has been archived by the owner on Oct 19, 2018. It is now read-only.

On Models and Stores

Mitch VanDuyn edited this page Jan 18, 2017 · 4 revisions

Struggling with the difference between models, flux stores and classes

Here is the world's simplest flux store

// Single object representing list data and logic
var ListStore = {

    // Actual collection of model data
    items: []

};

// Tell the dispatcher we want to listen for *any*
// dispatched events

MicroEvent.mixin( ListStore );

AppDispatcher.register( function( payload ) {

    switch( payload.actionName ) {

        // Do we know how to handle this action?
        case 'new-item':

            // We get to mutate data!
            ListStore.items.push( payload.newItem );
            // Tell the world we changed!
            ListStore.trigger( 'change' );
            break;

    }

}); 

ListActions = {

    add: function( item ) {
        AppDispatcher.dispatch({
            eventName: 'new-item',
            newItem: item
        });
    }

};

If we want to use a Dispatcher we would say:

module ListActions
  class Add < HyperEvent
    param :item
  end
end

class ListStore
  private_state list: [], scope: :class
  receives ListActions::Add { |item| state.list! << item }
end

Or because using the dispatcher is optional in Hyperloop we can shorten this to

class ListStore < HyperStore::Base
  private_state list: [], scope: :class
  def add!(item)
    state.list! << item
  end
end

and let the Store be the only receiver of the add! event.

and with HyperMesh (ActiveRecord) all we have to say is

class List < ActiveRecord::Base
end

which would also persist the new items.

What is the difference between a "Store", a "Model", and a plain old "Class"? When should use dispatchable events, and when should I just include action events in my Stores?

I found this answer to be quite useful in thinking about this:

Stores are domain models, not ORM models.

They manage application state for a logical domain. They can manage state using collections, single values or a combination of both.

But they have a number of specific features that set them apart from normal models:

  1. They have no setters. No one changes the stores from the outside.
  2. The only way data gets into stores is through the callback they register with the dispatcher. They receive every action that goes through the system through this callback. They define which actions they will respond to, ignoring most of them.
  3. The only methods they publicly expose are a set of getters and methods to register/unregister listeners.
  4. When the state they manage changes, they emit a change event. This alerts the view layer that the state of the store has changed, so the views can query for the new data they need, using the getters. They can call for new data, but when that data is returned it should be in the form of a new action so that all the stores may respond to it. Doing this ensures the resiliency of the application -- you always have all new data throughout the entire system.

Some people prefer to call for new data in the action creators, rather than the stores, which enforces that the new data will originate with an action. This is perfectly acceptable and actually is more common, I believe. But really either style is fine.

How does Hyperloop relate to this? The simple answer is that Hyperloop agrees with the true spirit and rationale behind this but greatly simplifies the actual implementation of these principles.

Let's take it apart section by section.

Stores are domain models, not ORM models.

Hyperloop says: *Stores are domain models, not necessarily ORM models. In Hyperloop ORM models can be Stores. Hyperloop can automatically create server side persisted stores using ActiveRecord models to define the persistence, scopes, relationships, and helper methods. Of course, you can have plain HyperStores that are not associated with a server-side model.

But they have a number of specific features that set them apart from normal models:

  • They have no setters. No one changes the stores from the outside.

Hyperloop says: Like any well-crafted class, a Store should not expose its internal data representation. This is particularly important when it comes to updating the internal state. Consider the above List store example. In the JS example, I would say ListActions.add(12) to add 12 to my List store. With a HyperStore I would say ListStore.add!(12). Both in effect change the store's state, but in a way that protects the client from knowing any details of how the Store is implemented. I may be missing something here, but I cannot see what value the Dispatcher adds. Why not just put the actions in a class like HyperStore does? Are we missing something?

  • The only way data gets into stores is through the callback they register with the dispatcher. They receive every action that goes through the system through this callback. They define which actions they will respond to, ignoring most of them.

Hyperloop says: Methods that will change the internal state of Stores by convention have names ending with a bang (!). This alerts the client that invoking this method will result in state changes, that will result in subsequent rerendering of any clients depending on the Store's state. The description above is actually an implementation detail. The real critical thing here is that there is some naming convention so that the developer can easily identify an action. In the JS Flux case the actions are grouped under objects named "...Actions".

  • The only methods they publicly expose are a set of getters and methods to register/unregister listeners.

Hyperloop says: Well it just says what it says above, and by the way listener registration is automatic.

  • When the state they manage changes, they emit a change event. This alerts the view layer that the state of the store has changed, so the views can query for the new data they need, using the getters. ...

Hyperloop says: Stores keep their internal state in reactive state variables. When the store updates its state it uses the state's bang (!) method, which will take care of notifying the view layer of any dependent views that the state has changed. Notice the consistent use of the bang: An action method ends with ! and it will without doubt internally update states using the state's ! method.

Some people prefer to call for new data in the action creators, rather than the stores, which enforces that the new data will originate with an action. This is perfectly acceptable and actually is more common, I believe. But really either style is fine.

Hyperloop takes the above philosophy. If the store needs to be updated (say via an API call) there should be an action method (named with a bang please) that will update the stores internal state. See the GitHubUserStream store for example.

Summary

HyperStores are domain models that may or may not be ORM models. In Hyperloop your rails ActiveRecord models are a type of HyperStore where the ActiveRecord class is used to define the persistence, scopes, relationships, and helper methods. Of course, you can have plain HyperStores that are not associated with a server-side model.

The key feature that sets a HyperStore apart from a regular ruby Class is that its internal state is stored in reactive state variables. These work just like normal instance variables except that they will cause the view layer to rerender when they change. The bookkeeping to track who gets updated, when what state changes is all done internally by HyperStore.

Any methods on a HyperStore that will reactively update state are called actions, and by convention have names that end in an exclamation (!) mark.

Comparing HyperStore with a conventional Flux Store.

Concept Flux HyperStore
models vs. stores Flux typically distinguishes between ORM models and stores An ActiveRecord model is a kind of HyperStore, so where desired database persistence can be automatic
setter methods and other side effects Anything with a side effect is defined into an action that goes through a central dispatcher. Actions may be grouped into Action Creators. HyperStore methods that have side effects are also called Actions, for clarity should have names ending with exclamation marks. HyperStore's are domain models and so the class definition should provide a protocol definition with both getters and actions in one place.
The dispatcher Stores register with dispatcher so they can receive actions. Adding a dispatcher allows multiple stores to participate in an action. We don't have this capability in Hyperloop. Do we need it? It could easily be added to Operations - I knew they should be called actions :-)
listeners views explicitly "listen" to state changes on stores A view is automatically registered as a listener if it depends on the current state of a store.
change events When flux stores change they must broadcast the change by some event mechanism HyperStore state variables have setter methods, that will automatically broadcast changes to the state as changes are made. Under the hood its just like normal flux, but just takes less work on the programmers part.
Clone this wiki locally