Skip to content

Latest commit

 

History

History
123 lines (85 loc) · 6.25 KB

File metadata and controls

123 lines (85 loc) · 6.25 KB

Stores

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 Proc as a param to the child from the parent that the child can call to 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
end

In 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)

The observe and mutate methods

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 mutate an instance variable outside of a Component, you need to observe it 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)

Helper methods

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.

  • observer
  • mutator
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_reader
  • state_writer
  • state_accessor

Finally there is the toggle method which does what it says on the tin.

  • toggle toggle(: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
end

Initializing class variables in singleton Store

You 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