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

Do We Need a Dispatcher?

Mitch VanDuyn edited this page Jan 19, 2017 · 7 revisions

Spoiler Alert: The answer is no, but lets add one anyway

The classic flux includes a Dispatcher.

What is it for?

Here is a good summary: https://facebook.github.io/react/blog/2014/07/30/flux-actions-and-the-dispatcher.html.

Bottom line: The dispatcher allows stores to be decoupled from actions. Instead of directly calling a store's action, the dispatcher allows an action to be its own object, to which stores can subscribe (like an event.) This way a component can say UpdateCountry('australia') and have several stores who care about "country" update their internal state. Otherwise, you might have to say: Country.update('australia') followed by something like City.update(Country.default_city).

Moving forward we have three choices:

  1. Mainline the dispatcher, and make using it the course of least resistance to act on a HyperStore.
  2. Add a dispatcher, but make it an equal partner with the alternatives.
  3. Ignore the dispatcher.

Mainline the Dispatcher

I don't think we want to do this.

  1. If the event is really related to a specific store, then adding the Actions as separate objects just adds additional code, which serves no purposes, and in fact obscures what is going on.
  2. It pretty much forces Stores to be singletons. This is a given in standard flux, but it has not been assumed in Hyperloop, and we have good examples where it's not appropriate.

Have a look at the UserIconStream example, and imagine writing this as strict flux store. It just adds a lot of cruft for no purpose.

Make the Dispatcher equal partners with other action mechanisms.

Currently, if you want to invoke an action, you call a method (either class or instance) on the store. Stores can then call other stores, as needed.

You can also use Operations to group actions together. So for the case of updating a country (as shown above) you would have an Operation called UpdateCountry whose execute method simply updated the country and city stores.

If you view Actions as a specialized Operation, where the Stores register themselves with the operation then it fits well with the rest of Hyperloop.

Ignore the Dispatcher

The reason for not adding the dispatcher would be to keep things simple. Hyperloop follows the Ruby philosophy of providing several ways to get the job done. Sometimes we can get carried away with this. Adding the dispatcher just adds more choices, and of course is more code to maintain.

That said there are cases where having dispatchable actions would simplify things.

It would seem the middle ground of including a dispatch mechanism that is integrated into Hyperloop is our best fit.

What would it look like?

Thinking about Actions as a type of Operation or indeed Operations as a special case of Actions, leads to merging them into a single HyperAction class.

HyperAction will work just like an Operation as understood today, *except HyperAction will have a predefined execute method that broadcasts the occurrence of the Action to all registered receivers.

To create a custom Operation you would subclass HyperAction butt add your own execute method just like Operations are currently understood.

Here is the ever investigated "Keeping Track of Multiple Components" example rewritten using Actions:

class OpenForm < HyperAction
  # current_component returns the component invoking the action
  param form: current_component, type: React::Component::Base
end

class CloseForm < HyperAction
  param form: current_component, type: React::Component::Base
end

class CloseForms < HyperAction
end

class FormStore < HyperStore::Base

  private_state form_state: {}, scope: :class

  receives OpenForm do |form|
    state.form_state![form] = :open
  end

  receives CloseForm do |form|
    return unless state.form_state[form] == :open 
    state.form_state![form] = :closing
    after(2) { state.form_state!.delete(form) }
  end

  receives CloseForms do
    state.form_state.each_key { |form| CloseForm(form: form) }
  end

  def self.my_state(form = current_component)
    state.form_state[form] || :closed
  end
  
  %w(open closing).each do |method|
    define_method "#{method}?" { state.form_state.has_value? method }
  end
end

class AForm < React::Component::Base

  param :name

  before_unmount { CloseForm() }

  render(DIV) do
    "I am #{params.name} ".span
    case FormStore.my_state
    when :opened
      BUTTON { 'close me' }.on(:click) { CloseForm() }
    when :closed
      BUTTON { 'open me' }.on(:click) { OpenForm() }
    else
      SPAN { 'closing...' }
    end
  end
end

class App < React::Component::Base
  render(DIV) do
    AForm(name: 'form 1')
    AForm(name: 'form 2')
    if FormStore.closing?
      DIV { 'closing...' }
    elsif FormState.open?
      BUTTON { 'Close All' }.on(:click) { CloseAll() }
    end
  end
end

It actually reads as well as the "non-dispatched" version, and does remove the extra instance variable from AForm, and removes all the separate instance states from the store.

Plus another very nice advantage of this approach is that an Action's execute method can call super and "rebroadcast" the Action to any other registered receivers. So a HyperAction can act like both a Flux Action and a Trailblazer Operation at the same time.

Can Components Receive Actions?

Sure why not! Should they? Most of the time probably not.

Still allowing small systems to treat a component as a Store will be useful and might also help to simplify tutorials.

Remaining Questions

So now we are left with the following questions:

  • When do I use flux style "dispatched" actions versus Actions with custom execute methods?
  • When do use an action method in a store versus receiving an Action?
  • What is the difference between a Model, a Store, and some other ruby Class?
  • When should just let a component also be a Store
Clone this wiki locally