|
1 | 1 |
|
2 | | -### Param Equality |
| 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"> |
| 3 | +**The `Hyperstack::State::Observable` module allows you to build |
| 4 | +classes that share their state with Hyperstack Components, and have those components update when objects in those classes change state.** |
3 | 5 |
|
4 | | -Params can be arbitrarily |
5 | | - |
6 | | -### Params, Immutability and State |
7 | | - |
8 | | -Some care must be taken when passing params that are not simple scalars (string, numbers, etc) or JSON like |
9 | | -combinations of arrays and hashes. When passing more complex objects |
10 | | - |
11 | | -Hyperstack differs from React in how it deals with changes in param values |
12 | | - |
13 | | -A component will re-render when new param values are received. If it appears that parameter values have changed, |
14 | | -then the component will not re-render. For scalars such as strings and numbers and JSON like combinations of arrays |
15 | | -and hashes, the component will be re-rendered if the value of the param changes. |
16 | | - |
17 | | -For more complex objects such as application defined classes, there is generally no way to easily determine that an |
18 | | -object's value has changed. Hyperstack solves this problem with the `Observable` class, that allows objects to |
19 | | -track which components are depending on their values |
20 | | - |
21 | | -In React \(and Hyperstack\) state is mutable. Changes \(mutations\) to state variables cause Components to re-render. Where state is passed into a child Component as a `param`, it will cause a re-rendering of that child Component. Change flows from a parent to a child - change does not flow upward and this is why params are not mutable. |
22 | | - |
23 | | -State variables are normal instance variables or objects. When a state variable changes, we use the `mutate` method to get React's attention and cause a re-render. Like normal instance variables, state variables are created when they are first accessed, so there is no explicit declaration. |
24 | | - |
25 | | -The syntax of `mutate` is simple - its `mutate` and any other number of parameters and/or a block. Normal evaluation means the parameters are going to be evaluated first, and then `mutate` gets called. |
26 | | - |
27 | | -* `mutate @foo = 12, @bar[:zap] = 777` executes the two assignments first, then calls mutate |
28 | | -* or you can say `mutate { @foo = 12; @bar[:zap] = 777 }` which is more explicit, and does the same thing |
29 | | - |
30 | | -Here are some examples: |
31 | | - |
32 | | -```ruby |
33 | | -class Counter < HyperComponent |
34 | | - before_mount do |
35 | | - @count = 0 # optional initialization |
36 | | - end |
37 | | - |
38 | | - render(DIV) do |
39 | | - # note how we mutate count |
40 | | - BUTTON { "+" }.on(:click) { mutate @count += 1) } |
41 | | - P { @count.to_s } |
42 | | - end |
43 | | -end |
44 | | -``` |
45 | | - |
46 | | -```ruby |
47 | | -class LikeButton < HyperComponent |
48 | | - render(DIV) do |
49 | | - BUTTON do |
50 | | - "You #{@liked ? 'like' : 'haven\'t liked'} this. Click to toggle." |
51 | | - end.on(:click) do |
52 | | - mutate @liked = !@liked |
53 | | - end |
54 | | - end |
55 | | -end |
56 | | -``` |
57 | | - |
58 | | -### Components are Just State Machines |
59 | | - |
60 | | -React thinks of UIs as simple state machines. By thinking of a UI as being in various states and rendering those states, it's easy to keep your UI consistent. |
61 | | - |
62 | | -In React, you simply update a component's state, and then the new UI will be rendered on this new state. React takes care of updating the DOM for you in the most efficient way. |
63 | | - |
64 | | -### What Components Should Have State? |
65 | | - |
66 | | -Most of your components should simply take some params and render based on their value. However, sometimes you need to respond to user input, a server request or the passage of time. For this you use state. |
67 | | - |
68 | | -**Try to keep as many of your components as possible stateless.** By doing this you'll isolate the state to its most logical place and minimize redundancy, making it easier to reason about your application. |
69 | | - |
70 | | -A common pattern is to create several stateless components that just render data, and have a stateful component above them in the hierarchy that passes its state to its children via `param`s. The stateful component encapsulates all of the interaction logic, while the stateless components take care of rendering data in a declarative way. |
71 | | - |
72 | | -State can be held in any object \(not just a Component\). For example: |
73 | | - |
74 | | -```ruby |
75 | | -class TestIt |
76 | | - def self.swap_state |
77 | | - @@test = !@@test |
78 | | - end |
79 | | - |
80 | | - def self.result |
81 | | - @@test ? 'pass' : 'fail' |
82 | | - end |
83 | | -end |
84 | | - |
85 | | -class TestResults < HyperComponent |
86 | | - render(DIV) do |
87 | | - P { "Test is #{TestIt.result}" } |
88 | | - BUTTON { 'Swap' }.on(:click) do |
89 | | - mutate TestIt::swap_state |
90 | | - end |
91 | | - end |
92 | | -end |
93 | | -``` |
94 | | - |
95 | | -In the example above, the singleton class `TestIt` holds its own internal state which is changed through a `swap_state` class method. The `TestResults` Component has no knowledge of the internal workings of the `TestIt` class. |
96 | | - |
97 | | -When the BUTTON is pressed, we call `mutate`, passing the object which is being mutated. The actual mutated value is not important, it is the fact that the _observed_ object \(our `TestIt` class\) is being mutated that will cause a re-render of the _observing_ `TestResults` Component. Think about `mutate` as a way of telling React that the Component needs to be re-rendered as the state has changed. |
98 | | - |
99 | | -In the example above, we could also move the _observing_ and _mutating_ behaviour out of the Component completely and manage it in the `TestIt` class - in this case, we would call it a Store. Stores are covered in the Hyper-Store documentation later. |
100 | | - |
101 | | -### What Should Go in State? |
102 | | - |
103 | | -**State should contain data that a component's instance variables, event handlers, timers, or http requests may change and trigger a UI update.** |
104 | | - |
105 | | -When building a stateful component, think about the minimal possible representation of its state, and only store those properties in `state`. Add to your class methods to compute higher level values from your state variables. Avoid adding redundant or computed values as state variables as these values must then be kept in sync whenever state changes. |
106 | | - |
107 | | -### What Shouldn't Go in State? |
108 | | - |
109 | | -State should contain the minimal amount of data needed to represent your UI's state. As such, it should not contain: |
110 | | - |
111 | | -* **Computed data:** Don't worry about precomputing values based on state — it's easier to ensure that your UI is consistent if you do all computation during rendering. For example, if you have an array of list items in state and you want to render the count as a string, simply render `"#{@list_items.length} list items'` in your `render` method rather than storing the count as another state. |
112 | | -* **Data that does not effect rendering:** Changing an instance variable \(or any object\) that does not affect rendering does not need to be mutated \(i.e you do not need to call `mutate`\). |
113 | | - |
114 | | -The rule is simple: anytime you are updating a state variable use `mutate` and your UI will be re-rendered appropriately. |
115 | | - |
116 | | -### State and user input |
117 | | - |
118 | | -Often in a UI you gather input from a user and re-render the Component as they type. For example: |
119 | | - |
120 | | -```ruby |
121 | | -class UsingState < HyperComponent |
122 | | - |
123 | | - render(DIV) do |
124 | | - # the button method returns an HTML element |
125 | | - # .on(:click) is an event handeler |
126 | | - # notice how we use the mutate method to get |
127 | | - # React's attention. This will cause a |
128 | | - # re-render of the Component |
129 | | - button.on(:click) { mutate(@show = !@show) } |
130 | | - DIV do |
131 | | - input |
132 | | - output |
133 | | - easter_egg |
134 | | - end if @show |
135 | | - end |
136 | | - |
137 | | - def button |
138 | | - BUTTON(class: 'ui primary button') do |
139 | | - @show ? 'Hide' : 'Show' |
140 | | - end |
141 | | - end |
142 | | - |
143 | | - def input |
144 | | - DIV(class: 'ui input fluid block') do |
145 | | - INPUT(type: :text).on(:change) do |evt| |
146 | | - # we are updating the value per keypress |
147 | | - # using mutate will cause a rerender |
148 | | - mutate @input_value = evt.target.value |
149 | | - end |
150 | | - end |
151 | | - end |
152 | | - |
153 | | - def output |
154 | | - # rerender whenever input_value changes |
155 | | - P { "#{@input_value}" } |
156 | | - end |
157 | | - |
158 | | - def easter_egg |
159 | | - H2 {'you found it!'} if @input_value == 'egg' |
160 | | - end |
161 | | -end |
162 | | -``` |
163 | | - |
164 | | -### State and HTTP responses |
165 | | - |
166 | | -Often your UI will re-render based on the response to a HTTP request to a remote service. Hyperstack does not need to understand the internals of the HTTP response JSON, but does need to _observe_ the object holding that response so we call `mutate` when updating our response object in the block which executes when the HTTP.get promise resolves. |
167 | | - |
168 | | -```ruby |
169 | | -class FaaS < HyperComponent |
170 | | - render(DIV) do |
171 | | - BUTTON { 'faastruby.io' }.on(:click) do |
172 | | - faast_ruby |
173 | | - end |
174 | | - |
175 | | - DIV(class: :block) do |
176 | | - P { @hello_response['function_response'].to_s } |
177 | | - P { "executed in #{@hello_response['execution_time']} ms" } |
178 | | - end if @hello_response |
179 | | - end |
180 | | - |
181 | | - def faast_ruby |
182 | | - HTTP.get('https://api.faastruby.io/paulo/hello-world', |
183 | | - data: {time: true} |
184 | | - ) do |response| |
185 | | - # this code executes when the promise resolves |
186 | | - # notice that we call mutate when updating the state instance variable |
187 | | - mutate @hello_response = response.json if response.ok? |
188 | | - end |
189 | | - end |
190 | | -end |
191 | | -``` |
192 | | - |
193 | | -### State and updating interval |
194 | | - |
195 | | -One common use case is a component wanting to update itself on a time interval. It's easy to use the kernel method `every`, but it's important to cancel your interval when you don't need it anymore to save memory. Hyperstack provides Lifecycle Methods \(covered in the next section\) that let you know when a component is about to be created or destroyed. Let's create a simple mixin that uses these methods to provide a React friendly `every` function that will automatically get cleaned up when your component is destroyed. |
196 | | - |
197 | | -```ruby |
198 | | -module ReactInterval |
199 | | - |
200 | | - def self.included(base) |
201 | | - base.before_mount do |
202 | | - @intervals = [] |
203 | | - end |
204 | | - |
205 | | - base.before_unmount do |
206 | | - @intervals.each(&:stop) |
207 | | - end |
208 | | - end |
209 | | - |
210 | | - def every(seconds, &block) |
211 | | - Kernel.every(seconds, &block).tap { |i| @intervals << i } |
212 | | - end |
213 | | -end |
214 | | - |
215 | | -class TickTock < HyperComponent |
216 | | - include ReactInterval |
217 | | - |
218 | | - before_mount do |
219 | | - @seconds = 0 |
220 | | - end |
221 | | - |
222 | | - after_mount do |
223 | | - every(1) { mutate @seconds = @seconds + 1 } |
224 | | - end |
225 | | - |
226 | | - render(DIV) do |
227 | | - P { "Hyperstack has been running for #{@seconds} seconds" } |
228 | | - end |
229 | | -end |
230 | | -``` |
231 | | - |
232 | | -Notice that TickTock effectively has two `before_mount` methods, one that is called to initialize the `@intervals` array and another to initialize `@seconds` |
233 | | - |
234 | | - |
235 | | - |
236 | | -# Stores |
237 | | - |
238 | | -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. |
239 | | - |
240 | | -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. |
241 | | - |
242 | | -Some examples of where this can be necessary are: |
243 | | - |
244 | | -* 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. |
245 | | -* 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. |
246 | | -* 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. |
247 | | - |
248 | | -Taking each of these examples, there are ways to accomplish each: |
249 | | - |
250 | | -* 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.\) |
251 | | -* 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. |
252 | | -* 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.** |
253 | | - |
254 | | -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\). |
255 | | - |
256 | | -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\). |
257 | | - |
258 | | -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. |
259 | | - |
260 | | -```ruby |
261 | | -# app/hyperstack/stores/item_store.rb |
262 | | -class ItemStore |
263 | | - include Hyperstack::State::Observable |
264 | | - |
265 | | - class << self |
266 | | - def filter=(f) |
267 | | - mutate @filter = f |
268 | | - end |
269 | | - |
270 | | - def filter |
271 | | - observe @filter || '' |
272 | | - end |
273 | | - end |
274 | | -end |
275 | | -``` |
276 | | - |
277 | | -In Our application code, we would use the filter like this: |
278 | | - |
279 | | -```ruby |
280 | | -# the TextField on the Menu Bar could look like this: |
281 | | -TextField(label: 'Filter', value: ItemStore.filter).on(:change) do |e| |
282 | | - ItemStore.filter = e.target.value |
283 | | -end |
284 | | - |
285 | | -# elsewhere in the code we could use the filter to decide if an item is added to a list |
286 | | -show_item(item) if item.name.include?(ItemStore.filter) |
287 | | -``` |
288 | | - |
289 | | -## The observe and mutate methods |
290 | | - |
291 | | -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. |
292 | | - |
293 | | -`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. |
294 | | - |
295 | | -> 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. |
296 | | -
|
297 | | -The `observe` and `mutate` methods take: |
298 | | - |
299 | | -* a single param as shown above |
300 | | -* a string of params \(`mutate a=1, b=2`\) |
301 | | -* or a block in which case the entire block will be executed before signalling the rest of the system |
302 | | -* no params \(handy for adding to the end of a method\) |
303 | | - |
304 | | -## Helper methods |
305 | | - |
306 | | -To make things easier the `Hyperstack::State::Observable` mixin contains some useful helper methods: |
307 | | - |
308 | | -The `observer` and `mutator` methods create a method wrapped in `observe` or `mutate` block. |
309 | | - |
310 | | -* `observer` |
311 | | -* `mutator` |
312 | | - |
313 | | -```ruby |
314 | | -mutator(:inc) { @count = @count + 1 } |
315 | | -mutator(:reset) { @count = 0 } |
316 | | -``` |
317 | | - |
318 | | -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. |
319 | | - |
320 | | -* `state_reader` |
321 | | -* `state_writer` |
322 | | -* `state_accessor` |
323 | | - |
324 | | -Finally there is the `toggle` method which does what it says on the tin. |
325 | | - |
326 | | -* `toggle` toggle\(:foo\) === mutate @foo = !@foo |
327 | | - |
328 | | -```ruby |
329 | | -class ClickStore |
330 | | - include Hyperstack::State::Observable |
331 | | - |
332 | | - class << self |
333 | | - observer(:count) { @count ||= 0 } |
334 | | - state_writer :count |
335 | | - mutator(:inc) { @count = @count + 1 } |
336 | | - mutator(:reset) { @count = 0 } |
337 | | - end |
338 | | -end |
339 | | -``` |
340 | | - |
341 | | -### Initializing class variables in singleton Store |
342 | | - |
343 | | -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: |
344 | | - |
345 | | -```ruby |
346 | | -class CardStore |
347 | | - include Hyperstack::State::Observable |
348 | | - |
349 | | - @show_card_status = true |
350 | | - @show_card_details = false |
351 | | - |
352 | | - class << self |
353 | | - state_accessor :show_card_status |
354 | | - state_accessor :show_card_details |
355 | | - end |
356 | | -end |
357 | | -``` |
| 6 | +## This Page Under Construction |
0 commit comments