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

The Structure of a Hyperloop Application

Mitch VanDuyn edited this page Jan 20, 2017 · 5 revisions

Hyperloop is not a Framework...

Unless you want it to be. Hyperloop is a collection of Ruby classes packaged in several gems so that you can quickly (and we mean quickly) create great applications. These classes allow you to structure your code so that it's reusable, maintainable, and best of all short and sweet. If you want to call that a framework, that is okay, but its very useful also think about how each of the Hyperloop elements works as an independent part of the application. That way you can pick and choose how and when to use each piece of Hyperloop to your best advantage.

So without further ado, let's get stuck into the Structure of a Hyperloop Application.

Stores

Sitting at the base of your Application are one or more Stores. This is the same "Store" that Flux talks about. In Hyperloop, Stores are created by subclassing the HyperStore base class.

A HyperStore is just like any other Ruby class but with a very special feature: reactive instance variables or state variables, or simply state.

The state literally holds the state of your application. State variables work just like regular Ruby instance variables, except that when they change they intelligently inform any of your Components (we will get to them next) that they will need to rerender with the new state.

Here is a very simple Store which stores a list of Words:

class Word < HyperStore::Base
  private_state list: [], scope: :class
  def self.add!(word)
    state.list! << word
  end
  def self.all
    state.list
  end
end

The declaration private_state list: [], scope: :class creates a state variable called list, and initializes it to an empty array. The scope: :class option indicates that the state will be associated with the class (instead of having a separate state variable for every instance of the class.)

The class method add! will push a new word onto the words list. We tell HyperStore that we are acting on the value of words by appending the exclamation to the state name. Likewise, so users of our Word store will know that the add method is an action we by convention add an exclamation to its name as well.

To see our words we have the all method. Notice that in this case we are only reading the state so there is no ! either on our method name, or when we access the state.

Besides private_state you can also declare a state_reader which will build the reader method using the states name. For example if we could have said state_reader all... and would automatically get the all method declared. However, we would have to then change our code to use state.all (instead of state.list), and I wouldn't have had this nice teaching moment with you.

Except for the receives method which will get to later under Actions that is all there is to know about HyperStore. The rest is plain old Ruby code. You can create Stores as simple as the one above or as complex as one that keeps a pool of random users available for display like this example.

Components

Now that we have some state tucked away we will want to display it. In Hyperloop you create the UI with Components which are Ruby classes wrapping React.js components. Let's make a simple component to display our Words, and allow the user to add more

class ShowWords < React::Component::Base
  render(DIV) do
    INPUT(placeholder: 'enter a new word, and hit enter', style: {width: 200})
    .on(:key_down) do |evt|
      if evt.key_code == 13
        Word.add! evt.target.value
        evt.target.value = ''
      end
    end
    UL do
      Word.all.each { |word| LI { word } }
    end
  end
end

Our definition is straight forward. Our ShowWords component renders a DIV which contains an input box, and a list of words.

The input box has a handler for the key_down event. If the key pressed is the enter key, we send the new word to Word's add! action method, and clear the input.

To show the list of words we use Word's all method and display each word in the list.

As new words are rendered, our component will rerender because the contents of all will have changed.

Hyperloop Components have all the features of normal React components and more. For the complete description of the DSL (domain specific language) and other details see the documentation.

HyperActions

So far in our example, we have defined the "Add" action directly in the Store. It is sometimes useful to structure the actions as separate entities.

  • Actions defined as separate entities allow Components to be completely separate from the Store being updated, which can facilitate reuse.
  • Actions defined as separate entities can access multiple stores, and do more complex tasks.

In the classic Flux pattern, all actions are defined as separate entities. Hyperloop is much less opinionated. In cases where the action is clearly associated with a specific Store, then go right ahead and define it as an action method in the store. If on the other hand the Action has a general meaning which can be described without reference to a particular store or state, then it probably should be created as a separate entity.

So for our example let's add a ClearAll action. The meaning is for any store to reset it self to its initial state.

First we define the Action:

class ClearAll < HyperAction
end

Then let's have our Word store listen for it:

class Word < HyperStore::Base
  receives ClearAll { state.list! [] }
end

Finally, let's create a new component that uses our ShowWords component and adds a 'Reset' button:

class App < React::Component::Base
  render(DIV) do
    ShowWords()
    BUTTON { 'Reset' }.on(:click) { ClearAll() }
  end
end

As I hope is clear, the ClearAll (no pun intended) Action breaks the coupling between the App component and the Word store. If we added another store that needed to respond to the ClearAll action only that store's code would change.

Action Parameters

Actions can have parameters which are defined using the param macro. For example if we wanted to go ahead and define our Word.add! action more traditionally we could say:

class AddWord < HyperAction
  param :word
end

and send it saying:

  AddWord(word: 'hello')

and receive it like this:

class Word < HyperStore::Base receives AddWord { |word| state.list! << word } end

The Action execute method

Every Action has an execute method. By default this method dispatches the action to all the registered receivers. You can add your own logic by defining the execute method yourself:

class ClearAll < HyperAction def execute # add some console debugging before we dispatch puts "ClearAll called at #{Time.now}" super # call super to run the base dispatcher end end

The execute method is the perfect place to put all sorts of logic that for example you might put into an controller. If you are familar with Trailblazer's Operations, this Actions are the same idea.

For example:

class AddRandomWord < HyperAction
  def execute
    
Clone this wiki locally