A core concept behind React is that Components contain their own state and pass state down to their children as params. React re-renders the interface based on those state changes. Each Component is discreet and only needs to worry about how to render itself and pass state down to its children.
Sometimes however, at an application level, Components need to be able to share information or state in a way which does not adhere to this strict parent-child relationship.
Some examples of where this can be necessary are:
- Where a child needs to pass a message back to its parent. An example would be if the child component is an item in a list, it might need to inform it's parent that it has been clicked on.
- When Hyperstack models are passed as params, child components might change the values of fields in the model, which might be rendered elsewhere on the page.
- There has to be a place to store non-persisted, global application-level data; like the ID of the currently logged in user or a preference or variable that affects the whole UI.
Taking each of these examples, there are ways to accomplish each:
- Child passing a message to parent: the easiest way is to pass a
Procas a param to the child from the parent that the child cancallto pass a message back to the parent. This model works well when there is a simple upward exchange of information (a child telling a parent that it has been selected for example). You can read more about Params of type Proc in the Component section of these docs. If howevere, you find yourself adding overusing this method, or passing messages from child to grandparent then you have reached the limits of this method and a Store would be a better option (read about Stores in this section.) - Models are stores. An instance of a model can be passed between Components, and any Component using the data in a Model to render the UI will re-render when the Model data changes. As an example, if you had a page displaying data from a Model and let's say you have an edit button on that page (which invokes a Dialog (Modal) based Component which receives the model as a param). As the user edits the Model fields in Dialog, the underlying page will show the changes as they are made as the changes to Model fields will be observed by the parent Components. In this way, Models act very much like Stores.
- Stores are where global, application wide state can exist in singleton classes that all Components can access or as class instances objects which hold data and state. A Store is a class or an instance of a class which holds state variables which can affect a re-render of any Component observing that data.
In technical terms, a Store is a class that includes the include Hyperstack::State::Observable mixin, which just adds the mutate and observe primitive methods (plus helpers built on top of them).
In most cases, you will want class level instance variables that share data across components. Occasionally you might need multiple instances of a store that you can pass between Components as params (much like a Model).
As an example, let's imagine we have a filter field on a Menu Bar in our application. As the user types, we want the user interface to display only the items which match the filter. As many of the Components on the page might depend on the filter, a singleton Store is the perfect answer.
# app/hyperstack/stores/item_store.rb
class ItemStore
include Hyperstack::State::Observable
class << self
def filter=(f)
mutate @filter = f
end
def filter
observe @filter || ''
end
end
endIn Our application code, we would use the filter like this:
# the TextField on the Menu Bar could look like this:
TextField(label: 'Filter', value: ItemStore.filter).on(:change) do |e|
ItemStore.filter = e.target.value
end
# elsewhere in the code we could use the filter to decide if an item is added to a list
show_item(item) if item.name.include?(ItemStore.filter)As with Components, you mutate an instance variable to notify React that the Component might need to be re-rendered based on the state change of that object. Stores are the same. When you mutate and instance variable in Store, all Components that are observing that variable will be re-rendered.
observe records that a Component is observing an instance variable in a Store and might need to be re-rendered if the variable is mutated in the future.
If you
mutatean instance variable outside of a Component, you need toobserveit because, for simplicity, a Component observe their own instance vaibales.
The observe and mutate methods take:
- a single param as shown above
- a string of params (
mutate a=1, b=2) - or a block in which case the entire block will be executed before signalling the rest of the system
- no params (handy for adding to the end of a method)
To make things easier the Hyperstack::State::Observable mixin contains some useful helper methods:
The observer and mutator methods create a method wrapped in observe or mutate block.
observermutator
mutator(:inc) { @count = @count + 1 }
mutator(:reset) { @count = 0 }The state_accessor, state_reader and state_writer methods work just like attr_accessor methods except access is wrapped in the appropriate mutate or observe method. These methods can be used either at the class or instance level as needed.
state_readerstate_writerstate_accessor
Finally there is the toggle method which does what it says on the tin.
toggletoggle(:foo) === mutate @foo = !@foo
class ClickStore
include Hyperstack::State::Observable
class << self
observer(:count) { @count ||= 0 }
state_writer :count
mutator(:inc) { @count = @count + 1 }
mutator(:reset) { @count = 0 }
end
endYou can keep the logic around initialization in your Store. Remember that in Ruby your class instance variables can be initialized as the class is defined:
class CardStore
include Hyperstack::State::Observable
@show_card_status = true
@show_card_details = false
class << self
state_accessor :show_card_status
state_accessor :show_card_details
end
end