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

Do We Need a Dispatcher?

Mitch VanDuyn edited this page Jan 18, 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.

What would it look like?

Actions would be subclasses of HyperAction, and can contain optional param descriptions just like Operations.

A HyperStore would have a method receives that takes an Action class, and a block, symbol or proc.

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
    state.form_state.each_key { |form| CloseForm(form: form) }
  end

  def self.my_state(form = current_component)
    state.form_state[form]
  end
  
  def self.open?
    state.form_state.has_value? :open
  end

  def self.closing?
    state.form_state.has_value? :closing
  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.

Clone this wiki locally