HyperState

Revisiting the Tic Tac Toe Game

The easiest way to understand HyperState is by example. If you you did not see the Tic-Tac-Toe example, then please review it now, as we are going to use this to demonstrate how to use the Hyperstack::State::Observable module.
In our original Tic-Tac-Toe implementation the state of the game was stored in the DisplayGame component. State was updated by "bubbling up" events from lower level components up to DisplayGame where the event handler updated the state.
This is a nice simple approach but suffers from two issues:
  • Each level of lower level components must be responsible for bubbling up the events to the higher component.
  • The DisplayGame component is responsible for both managing state and displaying the game.
As our applications become larger we will want a way to keep each component's interface isolated and not dependent on the overall architecture, and to insure good separation of concerns.
The Hyperstack::State::Observable module allows us to put the game's state into a separate class which can be accessed from any component: No more need to bubble up events, and no more cluttering up our DisplayGame component with state management and details of the game's data structure.
Here is the game state and associated methods moved out of the DisplayGame component into its own class:
1
class Game
2
include Hyperstack::State::Observable
3
4
def initialize
5
@history = [[]]
6
@step = 0
7
end
8
9
observer :player do
10
@step.even? ? :X : :O
11
end
12
13
observer :current do
14
@history[@step]
15
end
16
17
state_reader :history
18
19
WINNING_COMBOS = [[0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6]]
20
21
def current_winner?
22
WINNING_COMBOS.each do |a, b, c|
23
return current[a] if current[a] && current[a] == current[b] && current[a] == current[c]
24
end
25
false
26
end
27
28
mutator :handle_click! do |id|
29
board = history[@step]
30
return if current_winner? || board[id]
31
32
board = board.dup
33
board[id] = player
34
@history = history[0..@step] + [board]
35
@step += 1
36
end
37
38
mutator(:jump_to!) { |step| @step = step }
39
end
Copied!
Let's go over the each of the differences from the code that was in the DisplayGame component.
1
class Game
2
include Hyperstack::State::Observable
Copied!
Game is now in its own class and includes Hyperstack::State::Observable. This adds a number of methods to Game that allows our class to become a reactive store. When Game interacts with other stores and components they will be updated as the state of Game changes.
1
def initialize
2
@history = [[]]
3
@step = 0
4
end
Copied!
In the original implementation we initialized the two state variables @history and @step in the before_mount callback. The same initialization is now in the initialize method which will be called when a new instance of the game is created. This will still be done in the DisplayGame before_mount callback (see below.)
1
observer :player do
2
@step.even? ? :X : :O
3
end
4
5
observer :current do
6
@history[@step]
7
end
Copied!
In the original implementation we had instance methods player and current. Now that Game is a separate class we define these methods using observer.
The observer method creates a method that is the inverse of mutator. While mutate (and mutator) indicate that state has been changed observe and observer indicate that state has been accessed outside the class.
1
attr_reader :history
Copied!
Just as we have mutate, mutator, and state_writer, we have observe, observer, and state_reader.
1
WINNING_COMBOS = [[0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6]]
2
3
def current_winner?
4
WINNING_COMBOS.each do |a, b, c|
5
return current[a] if current[a] && current[a] == current[b] && current[a] == current[c]
6
end
7
false
8
end
Copied!
We don't need any changes to current_winner?. It accesses the internal state through the current method so there is no need to explicitly make current_winner? an observer (but we could, without affecting anything.)
1
mutator :handle_click! do |id|
2
board = history[@step]
3
return if current_winner? || board[id]
4
5
board = board.dup
6
board[id] = player
7
@history = history[0..@step] + [board]
8
@step += 1
9
end
10
11
mutator(:jump_to!) { |step| @step = step }
12
end
Copied!
Finally we need no changes to the handle_click! and jump_to! mutators either.

The Updated DisplayGame Component

1
class DisplayGame < HyperComponent
2
before_mount { @game = Game.new }
3
def moves
4
return unless @game.history.length > 1
5
6
@game.history.length.times do |move|
7
LI(key: move) { move.zero? ? "Go to game start" : "Go to move ##{move}" }
8
.on(:click) { @game.jump_to!(move) }
9
end
10
end
11
12
def status
13
if (winner = @game.current_winner?)
14
"Winner: #{winner}"
15
else
16
"Next player: #{@game.player}"
17
end
18
end
19
20
render(DIV, class: :game) do
21
DIV(class: :game_board) do
22
DisplayCurrentBoard(game: @game)
23
end
24
DIV(class: :game_info) do
25
DIV { status }
26
OL { moves }
27
end
28
end
29
end
Copied!
The DisplayGame before_mount callback is still responsible for initializing the game, but it no longer needs to be aware of the internals of the game's state. It simply calls Game.new and stores the result in the @game instance variable. For the rest of the component's code we call the appropriate method on @game.
We will need to pass the entire game to DisplayBoard (we will see why shortly) so we will rename it to DisplayCurrentBoard.
As we will see DisplayCurrentBoard will be responsible for directly notifying the game that a user has clicked, so we do not need to handle any events coming back from DisplayCurrentBoard.

The DisplayCurrentBoard Component

1
class DisplayCurrentBoard < HyperComponent
2
param :game
3
4
def draw_square(id)
5
BUTTON(class: :square, id: id) { game.current[id] }
6
.on(:click) { game.handle_click!(id) }
7
end
8
9
render(DIV) do
10
(0..6).step(3) do |row|
11
DIV(class: :board_row) do
12
(row..row + 2).each { |id| draw_square(id) }
13
end
14
end
15
end
16
end
Copied!
The DisplayCurrentBoard component receives the entire game, and it will access the current board, using the current method, and will directly notify the game when a user clicks using the handle_click! method.
By having DisplayCurrentBoard directly deal with user actions, we simplify both components as they do not have to communicate back upwards via events. Instead we communicate through the central game store.

The Flux Loop

Rather than sending params down to lower level components, and having the components bubble up events, we have created a Flux Loop. The Game store holds the state, the top level component reads the state and sends it down to lower level components, those components update the Game state causing the top level component to re-rerender.
This structure greatly simplifies the structure and understanding of our components, and keeps each component functionally isolated.
Furthermore algorithms such as current_winner? now are neatly abstracted out into their own class.

Classes and Instances

If we are sure we will only want one game board, we could define Game with class methods like this:
1
class Game
2
include Hyperstack::State::Observable
3
4
class << self
5
def initialize
6
@history = [[]]
7
@step = 0
8
end
9
10
observer :player do
11
@step.even? ? :X : :O
12
end
13
14
observer :current do
15
@history[@step]
16
end
17
18
state_reader :history
19
20
WINNING_COMBOS = [[0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6]]
21
22
def current_winner?
23
WINNING_COMBOS.each do |a, b, c|
24
return current[a] if current[a] && current[a] == current[b] && current[a] == current[c]
25
end
26
false
27
end
28
29
mutator :handle_click! do |id|
30
board = history[@step]
31
return if current_winner? || board[id]
32
33
board = board.dup
34
board[id] = player
35
@history = history[0..@step] + [board]
36
@step += 1
37
end
38
39
mutator(:jump_to!) { |step| @step = step }
40
end
41
end
42
43
class DisplayBoard < HyperComponent
44
param :board
45
46
def draw_square(id)
47
BUTTON(class: :square, id: id) { board[id] }
48
.on(:click) { Game.handle_click!(id) }
49
end
50
51
render(DIV) do
52
(0..6).step(3) do |row|
53
DIV(class: :board_row) do
54
(row..row + 2).each { |id| draw_square(id) }
55
end
56
end
57
end
58
end
59
60
class DisplayGame < HyperComponent
61
def moves
62
return unless Game.history.length > 1
63
64
Game.history.length.times do |move|
65
LI(key: move) { move.zero? ? "Go to game start" : "Go to move ##{move}" }
66
.on(:click) { Game.jump_to!(move) }
67
end
68
end
69
70
def status
71
if (winner = Game.current_winner?)
72
"Winner: #{winner}"
73
else
74
"Next player: #{Game.player}"
75
end
76
end
77
78
render(DIV, class: :game) do
79
DIV(class: :game_board) do
80
DisplayBoard(board: Game.current)
81
end
82
DIV(class: :game_info) do
83
DIV { status }
84
OL { moves }
85
end
86
end
87
end
Copied!
Now instead of creating an instance and passing it around we call the class level methods on Game throughout.
The Hyperstack::State::Observable module will call any class level initialize methods in the class or subclasses before the first component mounts.
Note that with this approach we can go back to passing just the current board to DisplayBoard as DisplayBoard can directly access Game.handle_click! since there is only one game.

Thinking About Stores

To summarize: a store is simply a Ruby object or class that using the observe and mutate methods marks when its internal data has been observed by some other class, or when its internal data has changed.
When components render they observe stores throughout the system, and when those stores mutate the components will rerender.
You as the programmer need only to remember that public methods that read internal state must at some point during their execution declare this using observe, observer, state_reader or state_accessor methods. Likewise a method that changes internal state must declare this using mutate, mutator, state_writer or state_accessor methods.
If your store's methods access other stores, you do not need worry about their state, only your own. On the other hand keep in mind that the built in Ruby Array and Hash classes are not stores, so when you modify or read an Array or a Hash its up to you to use the appropriate mutate or observe method.

Stores and Parameters

Typically in a large system you will have one or more central stores, and what you end up passing as parameters are either instances of those stores, or some other kind of index into the store. If there is only one store (as in the case of our Game), you need not pass any parameters at all.
We can rewrite the previous iteration of DisplayBoard to demonstrate this:
1
class DisplaySquare
2
param :id
3
render
4
BUTTON(class: :square, id: id) { Game.current[id] }
5
.on(:click) { Game.handle_click(id) }
6
end
7
end
8
9
class DisplayBoard < HyperComponent
10
render(DIV) do
11
(0..6).step(3) do |row|
12
DIV(class: :board_row) do
13
(row..row + 2).each { |id| DisplaySquare(id: id) }
14
end
15
end
16
end
17
end
Copied!
Here DisplayBoard no longer takes any parameter (and could be renamed again to DisplayCurrentBoard) and now a new component - DisplaySquare - takes the id of the square to display, but the game or the current board are never passed as parameters; there is no need to as they are implicit.
Whether to pass (or not pass) a store class, an instance of a store, or some other index into the store is a design decision that depends on lots of factors, mainly how you see your application evolving over time.

Summary of Methods

All the observable methods can be used either at the class or instance level.

Observing State: observe, observer, state_reader

The observe method takes any number of arguments and/or a block. The last argument evaluated or the value of the block is returned.
The arguments and block are evaluated then the object's state will be observed.
If the block exits with a return or break, the state will not be observed.
1
# evaluate and return a value
2
observe @history[@step]
3
4
# evaluate a block and return its value
5
observe do
6
@history[@step]
7
end
Copied!
The observer method defines a new method with an implicit observe:
1
observer :foo do |x, y, z|
2
...
3
end
Copied!
is equivilent to
1
def foo(x, y, z)
2
observe do
3
...
4
end
5
end
Copied!
Again if the block exits with a return or break the state will not be observed.
The state_reader method declares one or more state accessors with an implicit state observation:
1
state_reader :bar, :baz
Copied!
is equivilent to
1
def bar
2
observe @bar
3
end
4
def baz
5
observe @baz
6
end
Copied!

Mutating State: mutate, mutator, state_writer, toggle

The mutate method takes any number of arguments and/or a block. The last argument evaluated or the value of the block is returned.
The arguments and block are evaluated then the object's state will be mutated.
If the block exits with a return or break, the state will not be mutated.
1
# evaluate and return a value
2
mutate @history[@step]
3
4
# evaluate a block and return its value
5
mutate do
6
@history[@step]
7
end
Copied!
The mutator method defines a new method with an implicit mutate:
1
mutator :foo do |x, y, z|
2
...
3
end
Copied!
is equivilent to
1
def foo(x, y, z)
2
mutate do
3
...
4
end
5
end
Copied!
Again if the block exits with a return or break the state will not be mutated.
The state_writer method declares one or more state accessors with an implicit state mutation:
1
state_reader :bar, :baz
Copied!
is equivilent to
1
def bar=(x)
2
mutate @bar = x
3
end
4
def baz=(x)
5
observe @baz = x
6
end
Copied!
The toggle method reverses the polarity of a instance variable:
1
toggle(:foo)
Copied!
is equivilent to
1
mutate @foo = !@foo
Copied!

The state_accessor Method

Combines state_reader and state_writer methods.
1
state_accessor :foo, :bar
Copied!
is equivilent to
1
state_reader :foo, :bar
2
state_writer :foo, :bar
Copied!

Components and Stores

The standard HyperComponent base class includes Hyperstack::State::Observable so any HyperComponent has access to all of the above methods. A component also always observes itself so you never need to use observe within a component unless the state will be accessed outside the component. However once you start doing that you would be better off to move the state into a separate store.
In addition components also act as the Observers in the system. What this means is that the current component that is running its render method is recording all stores that call observe, when a store mutates, then all the components that recorded observations will be rerendered.