-
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.
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 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.