Skip to content

Commit ee7f4f0

Browse files
committed
merged
1 parent 0a6b618 commit ee7f4f0

File tree

1 file changed

+111
-58
lines changed

1 file changed

+111
-58
lines changed

docs/hyper-state/README.md

Lines changed: 111 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11

2-
<img align="left" width="100" height="100" style="margin-right: 20px" src="https://github.com/hyperstack-org/hyperstack/blob/edge/docs/wip.png?raw=true" /> The `Hyperstack::State::Observable` module allows you to build classes that share their state with Hyperstack Components, and have those components update when objects in those classes change state.
3-
4-
## This Page Under Construction
5-
6-
The `Hyperstack::State::Observable` module allows you to build classes that share their state with Hyperstack Components and have those components update when objects in those classes change state.
72

83
### Revisiting the Tic Tac Toe Game
94

105
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](client-dsl/interlude-tic-tac-toe.md)**, as we are going to use this to demonstrate how to use the `Hyperstack::State::Observable` module.
116

127
In our original Tic-Tac-Toe implementation the state of the game was stored in the `DisplayGame` component. State was updated by
8+
<<<<<<< Updated upstream
139
"bubbling up" events from lower level components up to `DisplayGame` where the event handler updated the state.
10+
=======
11+
"bubbling up" events from lower level components up to `DisplayGame` where the event hander updated the state.
12+
>>>>>>> Stashed changes
1413
1514
This is a nice simple approach but suffers from two issues:
1615
+ Each level of lower level components must be responsible for bubbling up the events to the higher component.
@@ -19,11 +18,11 @@ This is a nice simple approach but suffers from two issues:
1918
As our applications become larger we will want a way to keep each component's interface isolated and not dependent on the overall
2019
architecture, and to insure good separation of concerns.
2120

22-
The `Hyperstack::State::Observable` module allows us to put the game's state into a separate class, which can be accessed from any
23-
component: No more need to bubble up events, and no more cluttering up our `DisplayGame` component with state management stuff
21+
The `Hyperstack::State::Observable` module allows us to put the game's state into a separate class which can be accessed from any
22+
component: No more need to bubble up events, and no more cluttering up our `DisplayGame` component with state management
2423
and details of the game's data structure.
2524

26-
Here is the game state moved out of the `DisplayGame` component into its own class:
25+
Here is the game state and associated methods moved out of the `DisplayGame` component into its own class:
2726

2827
```ruby
2928
class Game
@@ -73,9 +72,8 @@ class Game
7372
include Hyperstack::State::Observable
7473
```
7574

76-
Including `Hyperstack::State::Observable` gives us access to a number of methods that allows our class to become
77-
a *reactive store*, and interact with other stores and components so that when the `Game` state updates so will
78-
any components that directly or indirectly depend on its state.
75+
`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
76+
a *reactive store*. When `Game` interacts with other stores and components they will be updated as the state of `Game` changes.
7977

8078
```ruby
8179
def initialize
@@ -84,9 +82,9 @@ any components that directly or indirectly depend on its state.
8482
end
8583
```
8684

87-
In the original implementation we initialized the two state variables `@history` and `@step` in the `before_mount` callback.
88-
There is no "mounting" of a store, so instead we will initialize our game by creating a new instance in the `DisplayGame` before
89-
mount callback (see below.)
85+
In the original implementation we initialized the two state variables `@history` and `@step` in the `before_mount` callback. The same initialization
86+
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`
87+
`before_mount` callback (see below.)
9088

9189
```ruby
9290
observer :player do
@@ -108,8 +106,7 @@ been changed `observe` and `observer` indicate that state has been accessed outs
108106
attr_reader :history
109107
```
110108

111-
Just as we have `mutate`, `mutator`, and `state_writer`, we have `observe`, `observer`, and `state_reader`. The `state_accessor`
112-
method just combines the two together.
109+
Just as we have `mutate`, `mutator`, and `state_writer`, we have `observe`, `observer`, and `state_reader`.
113110

114111
```ruby
115112
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]]
@@ -178,7 +175,7 @@ end
178175

179176
The `DisplayGame` `before_mount` callback is still responsible for initializing the game, but it no longer needs to be aware of
180177
the internals of the game's state. It simply calls `Game.new` and stores the result in the `@game` instance variable. For the rest
181-
of the component call the appropriate method on `@game`.
178+
of the component's code we call the appropriate method on `@game`.
182179

183180
We will need to pass the entire game to `DisplayBoard` (we will see why shortly) so we will rename it to `DisplayCurrentBoard`.
184181

@@ -216,19 +213,20 @@ communicate back upwards via events. Instead we communicate through the central
216213

217214
Rather than sending params down to lower level components, and having the components bubble up events, we have created a *Flux Loop*.
218215
The `Game` store holds the state, the top level component reads the state and sends it down to lower level components, those
219-
components update the state, causing the top level component to re-rerender.
216+
components update the `Game` state causing the top level component to re-rerender.
220217

221218
This structure greatly simplifies the structure and understanding of our components, and keeps each component functionally isolated.
222219

223220
Furthermore algorithms such as `current_winner?` now are neatly abstracted out into their own class.
224221

225222
### Classes and Instances
226223

227-
If we are sure we will only want one Game board, we could define Game with class methods like this:
224+
If we are sure we will only want one game board, we could define `Game` with class methods like this:
228225

229226
```ruby
230227
class Game
231228
include Hyperstack::State::Observable
229+
232230
class << self
233231
def initialize
234232
@history = [[]]
@@ -286,7 +284,6 @@ class DisplayBoard < HyperComponent
286284
end
287285

288286
class DisplayGame < HyperComponent
289-
before_mount { Game.initialize }
290287
def moves
291288
return unless Game.history.length > 1
292289

@@ -316,33 +313,14 @@ class DisplayGame < HyperComponent
316313
end
317314
```
318315

319-
Note that with this approach we can go back to passing just the current board to `DisplayBoard` as `DisplayBoard` can
320-
directly access `Game.handle_click!` since there is only one game.
321-
322-
### The Boot Broadcast
323-
324-
Observable classes can also receive information from *broadcasts*. Hyperstack comes with one predefined broadcast: `Hyperstack::Application::Boot` which is sent when the application has finished loading but before the first component is mounted.
316+
Now instead of creating an instance and passing it around we
317+
call the class level methods on `Game` throughout.
325318

326-
We can hook into the Boot broadcast and replace the need for our top level component to call initialize:
319+
The `Hyperstack::State::Observable` module will call any class level `initialize` methods in the class or subclasses
320+
before the first component mounts.
327321

328-
```ruby
329-
class Game
330-
include Hyperstack::State::Observable
331-
332-
receives Hyperstack::Application::Boot do
333-
@history = [[]]
334-
@step = 0
335-
end
336-
337-
class << self
338-
...
339-
end
340-
end
341-
...
342-
```
343-
> Why is `receives` outside the class definitions? The receives method can be used to attach broadcasts to either class level or
344-
instances. In this case we want to attach the receiver to Game, not to Game's singleton class. More on receivers and broadcasting
345-
in the chapter on Operations.
322+
Note that with this approach we can go back to passing just the current board to `DisplayBoard` as `DisplayBoard` can
323+
directly access `Game.handle_click!` since there is only one game.
346324

347325
### Thinking About Stores
348326

@@ -359,11 +337,9 @@ If your store's methods access other stores, you do not need worry about their s
359337
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
360338
the appropriate `mutate` or `observe` method.
361339

362-
> Why not make Arrays and Hashes stores? For efficiency. Its a comprimise solution.
363-
364340
### Stores and Parameters
365341

366-
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. Or if (as in the case of our Game) there is only one store, you
342+
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
367343
need not pass any parameters at all.
368344

369345
We can rewrite the previous iteration of `DisplayBoard` to demonstrate this:
@@ -387,8 +363,8 @@ class DisplayBoard < HyperComponent
387363
end
388364
```
389365

390-
Here `DisplayBoard` no longer takes any parameter (and should be renamed again to `DisplayCurrentBoard`) and now
391-
`DisplaySquare` takes the id of the square to display, but the game or the current board are never passed as parameters;
366+
Here `DisplayBoard` no longer takes any parameter (and could be renamed again to `DisplayCurrentBoard`) and now a new component -
367+
`DisplaySquare` - takes the id of the square to display, but the game or the current board are never passed as parameters;
392368
there is no need to as they are implicit.
393369

394370
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
@@ -398,11 +374,11 @@ lots of factors, mainly how you see your application evolving over time.
398374

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

401-
#### Observing State
377+
#### Observing State: `observe, observer, state_reader`
402378

403379
The `observe` method takes any number of arguments and/or a block. The last argument evaluated or the value of the block is returned.
404380

405-
The arguments and block are evaluated then the objects state will be *observed*.
381+
The arguments and block are evaluated then the object's state will be *observed*.
406382

407383
If the block exits with a return or break, the state will **not** be observed.
408384

@@ -416,7 +392,7 @@ observe do
416392
end
417393
```
418394

419-
The `observer` method defines a new method with an implicit observer call:
395+
The `observer` method defines a new method with an implicit observe:
420396

421397
```ruby
422398
observer :foo do |x, y, z|
@@ -449,10 +425,87 @@ def baz
449425
end
450426
```
451427

452-
#### Mutating State
428+
#### Mutating State: `mutate, mutator, state_writer, toggle`
429+
430+
The `mutate` method takes any number of arguments and/or a block. The last argument evaluated or the value of the block is returned.
431+
432+
The arguments and block are evaluated then the object's state will be *mutated*.
433+
434+
If the block exits with a return or break, the state will **not** be mutated.
435+
436+
```ruby
437+
# evaluate and return a value
438+
mutate @history[@step]
439+
440+
# evaluate a block and return its value
441+
mutate do
442+
@history[@step]
443+
end
444+
```
445+
446+
The `mutator` method defines a new method with an implicit mutate:
447+
448+
```ruby
449+
mutator :foo do |x, y, z|
450+
...
451+
end
452+
```
453+
is equivilent to
454+
```ruby
455+
def foo(x, y, z)
456+
mutate do
457+
...
458+
end
459+
end
460+
```
461+
462+
Again if the block exits with a `return` or `break` the state will **not** be mutated.
463+
464+
The `state_writer` method declares one or more state accessors with an implicit state mutation:
465+
466+
```ruby
467+
state_reader :bar, :baz
468+
```
469+
is equivilent to
470+
```ruby
471+
def bar=(x)
472+
mutate @bar = x
473+
end
474+
def baz=(x)
475+
observe @baz = x
476+
end
477+
```
478+
479+
The `toggle` method reverses the polarity of a instance variable:
480+
481+
```ruby
482+
toggle(:foo)
483+
```
484+
is equivilent to
485+
```ruby
486+
mutate @foo = !@foo
487+
```
488+
489+
#### The `state_accessor` Method
490+
491+
Combines `state_reader` and `state_writer` methods.
492+
493+
```ruby
494+
state_accessor :foo, :bar
495+
```
496+
is equivilent to
497+
```ruby
498+
state_reader :foo, :bar
499+
state_writer :foo, :bar
500+
```
501+
502+
### Components and Stores
453503

504+
The standard `HyperComponent` base class includes `Hyperstack::State::Observable` so any `HyperComponent` has access to
505+
all of the above methods. A component also always **observes itself** so you never need to use `observe` within
506+
a component **unless** the state will be accessed outside the component. However once you start doing that you
507+
would be better off to move the state into a separate store.
454508

455-
receives
456-
mutate, mutator, state_writer
457-
state_accessor
458-
toggle
509+
> In addition components also act as the **Observers** in the system. What this means is
510+
that current component that is running its render method is recording all stores that call `observe`, when
511+
a store mutates, then all the components that recorded observations will be rerendered.

0 commit comments

Comments
 (0)