-
Notifications
You must be signed in to change notification settings - Fork 18
Do We Need a Dispatcher?
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:
- Mainline the dispatcher, and make using it the course of least resistance to act on a HyperStore.
- Add a dispatcher, but make it an equal partner with the alternatives.
- Ignore the dispatcher.
I don't think we want to do this.
- 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.
- 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.
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.
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.
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.
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.
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