|
1 | 1 | # Signalize |
2 | 2 |
|
3 | | -Signalize is a Ruby port of the JavaScript-based [core Signals package](https://github.com/preactjs/signals) by the Preact project. Signals provides reactive variables, derived computed state, side effect callbacks, and batched updates. |
4 | | - |
5 | | -Additional context as provided by the original documentation: |
6 | | - |
7 | | -> Signals is a performant state management library with two primary goals: |
8 | | -> |
9 | | -> Make it as easy as possible to write business logic for small up to complex apps. No matter how complex your logic is, your app updates should stay fast without you needing to think about it. Signals automatically optimize state updates behind the scenes to trigger the fewest updates necessary. They are lazy by default and automatically skip signals that no one listens to. |
10 | | -> Integrate into frameworks as if they were native built-in primitives. You don't need any selectors, wrapper functions, or anything else. Signals can be accessed directly and your component will automatically re-render when the signal's value changes. |
11 | | -
|
12 | | -While a lot of what we tend to write in Ruby is in the form of repeated, linear processing cycles (aka HTTP requests/responses on the web), there is increasingly a sense that we can look at concepts which make a lot of sense on the web frontend in the context of UI interactions and data flows and apply similar principles to the backend as well. Signalize helps you do just that. |
13 | | - |
14 | | -**NOTE:** read the Contributing section below before submitting a bug report or PR. |
15 | | - |
16 | | -## Installation |
17 | | - |
18 | | -Install the gem and add to the application's Gemfile by executing: |
19 | | - |
20 | | - $ bundle add signalize |
21 | | - |
22 | | -If bundler is not being used to manage dependencies, install the gem by executing: |
23 | | - |
24 | | - $ gem install signalize |
25 | | - |
26 | | -## Usage |
27 | | - |
28 | | -Signalize's public API consists of five methods (you can think of them almost like functions): `signal`, `untracked`, `computed`, `effect`, and `batch`. |
29 | | - |
30 | | -### `signal(initial_value)` |
31 | | - |
32 | | -The first building block is the `Signalize::Signal` class. You can think of this as a reactive value object which wraps an underlying primitive like String, Integer, Array, etc. |
33 | | - |
34 | | -```ruby |
35 | | -require "signalize" |
36 | | - |
37 | | -counter = Signalize.signal(0) |
38 | | - |
39 | | -# Read value from signal, logs: 0 |
40 | | -puts counter.value |
41 | | - |
42 | | -# Write to a signal |
43 | | -counter.value = 1 |
44 | | -``` |
45 | | - |
46 | | -You can include the `Signalize::API` mixin to access these methods directly in any context: |
47 | | - |
48 | | -```ruby |
49 | | -require "signalize" |
50 | | -include Signalize::API |
51 | | - |
52 | | -counter = signal(0) |
53 | | - |
54 | | -counter.value += 1 |
55 | | -``` |
56 | | - |
57 | | -### `untracked { }` |
58 | | - |
59 | | -In case when you're receiving a callback that can read some signals, but you don't want to subscribe to them, you can use `untracked` to prevent any subscriptions from happening. |
60 | | - |
61 | | -```ruby |
62 | | -require "signalize" |
63 | | -include Signalize::API |
64 | | - |
65 | | -counter = signal(0) |
66 | | -effect_count = signal(0) |
67 | | -fn = proc { effect_count.value + 1 } |
68 | | - |
69 | | -effect do |
70 | | - # Logs the value |
71 | | - puts counter.value |
72 | | - |
73 | | - # Whenever this effect is triggered, run `fn` that gives new value |
74 | | - effect_count.value = untracked(&fn) |
75 | | -end |
76 | | -``` |
77 | | - |
78 | | -### `computed { }` |
79 | | - |
80 | | -You derive computed state by accessing a signal's value within a `computed` block and returning a new value. Every time that signal value is updated, a computed value will likewise be updated. Actually, that's not quite accurate — the computed value only computes when it's read. In this sense, we can call computed values "lazily-evaluated". |
81 | | - |
82 | | -```ruby |
83 | | -require "signalize" |
84 | | -include Signalize::API |
85 | | - |
86 | | -name = signal("Jane") |
87 | | -surname = signal("Doe") |
88 | | - |
89 | | -full_name = computed do |
90 | | - name.value + " " + surname.value |
91 | | -end |
92 | | - |
93 | | -# Logs: "Jane Doe" |
94 | | -puts full_name.value |
95 | | - |
96 | | -name.value = "John" |
97 | | -name.value = "Johannes" |
98 | | -# name.value = "..." |
99 | | -# Setting value multiple times won't trigger a computed value refresh |
100 | | - |
101 | | -# NOW we get a refreshed computed value: |
102 | | -puts full_name.value |
103 | | -``` |
104 | | - |
105 | | -### `effect { }` |
106 | | - |
107 | | -Effects are callbacks which are executed whenever values which the effect has "subscribed" to by referencing them have changed. An effect callback is run immediately when defined, and then again for any future mutations. |
108 | | - |
109 | | -```ruby |
110 | | -require "signalize" |
111 | | -include Signalize::API |
112 | | - |
113 | | -name = signal("Jane") |
114 | | -surname = signal("Doe") |
115 | | -full_name = computed { name.value + " " + surname.value } |
116 | | - |
117 | | -# Logs: "Jane Doe" |
118 | | -effect { puts full_name.value } |
119 | | - |
120 | | -# Updating one of its dependencies will automatically trigger |
121 | | -# the effect above, and will print "John Doe" to the console. |
122 | | -name.value = "John" |
123 | | -``` |
124 | | - |
125 | | -You can dispose of an effect whenever you want, thereby unsubscribing it from signal notifications. |
126 | | - |
127 | | -```ruby |
128 | | -require "signalize" |
129 | | -include Signalize::API |
130 | | - |
131 | | -name = signal("Jane") |
132 | | -surname = signal("Doe") |
133 | | -full_name = computed { name.value + " " + surname.value } |
134 | | - |
135 | | -# Logs: "Jane Doe" |
136 | | -dispose = effect { puts full_name.value } |
137 | | - |
138 | | -# Destroy effect and subscriptions |
139 | | -dispose.() |
140 | | - |
141 | | -# Update does nothing, because no one is subscribed anymore. |
142 | | -# Even the computed `full_name` signal won't change, because it knows |
143 | | -# that no one listens to it. |
144 | | -surname.value = "Doe 2" |
145 | | -``` |
146 | | - |
147 | | -**IMPORTANT:** you cannot use `return` or `break` within an effect block. Doing so will raise an exception (due to it breaking the underlying execution model). |
148 | | - |
149 | | -```ruby |
150 | | -def my_method(signal_obj) |
151 | | - effect do |
152 | | - return if signal_obj.value > 5 # DON'T DO THIS! |
153 | | - |
154 | | - puts signal_obj.value |
155 | | - end |
156 | | - |
157 | | - # more code here |
158 | | -end |
159 | | -``` |
160 | | - |
161 | | -Instead, try to resolve it using more explicit logic: |
162 | | - |
163 | | -```ruby |
164 | | -def my_method(signal_obj) |
165 | | - should_exit = false |
166 | | - |
167 | | - effect do |
168 | | - should_exit = true && next if signal_obj.value > 5 |
169 | | - |
170 | | - puts signal_obj.value |
171 | | - end |
172 | | - |
173 | | - return if should_exit |
174 | | - |
175 | | - # more code here |
176 | | -end |
177 | | -``` |
178 | | - |
179 | | -However, there's no issue if you pass in a method proc directly: |
180 | | - |
181 | | -```ruby |
182 | | -def my_method(signal_obj) |
183 | | - @signal_obj = signal_obj |
184 | | - |
185 | | - effect &method(:an_effect_method) |
186 | | - |
187 | | - # more code here |
188 | | -end |
189 | | - |
190 | | -def an_effect_method |
191 | | - return if @signal_obj.value > 5 |
192 | | - |
193 | | - puts @signal_obj.value |
194 | | -end |
195 | | -``` |
196 | | - |
197 | | -### `batch { }` |
198 | | - |
199 | | -You can write to multiple signals within a batch, and flush the updates at all once (thereby notifying computed refreshes and effects). |
200 | | - |
201 | | -```ruby |
202 | | -require "signalize" |
203 | | -include Signalize::API |
204 | | - |
205 | | -name = signal("Jane") |
206 | | -surname = signal("Doe") |
207 | | -full_name = computed { name.value + " " + surname.value } |
208 | | - |
209 | | -# Logs: "Jane Doe" |
210 | | -dispose = effect { puts full_name.value } |
211 | | - |
212 | | -batch do |
213 | | - name.value = "Foo" |
214 | | - surname.value = "Bar" |
215 | | -end |
216 | | -``` |
217 | | - |
218 | | -### `signal.subscribe { }` |
219 | | - |
220 | | -You can explicitly subscribe to a signal signal value and be notified on every change. (Essentially the Observable pattern.) In your block, the new signal value will be supplied as an argument. |
221 | | - |
222 | | -```ruby |
223 | | -require "signalize" |
224 | | -include Signalize::API |
225 | | - |
226 | | -counter = signal(0) |
227 | | - |
228 | | -counter.subscribe do |new_value| |
229 | | - puts "The new value is #{new_value}" |
230 | | -end |
231 | | - |
232 | | -counter.value = 1 # logs the new value |
233 | | -``` |
234 | | - |
235 | | -### `signal.peek` |
236 | | - |
237 | | -If you need to access a signal's value inside an effect without subscribing to that signal's updates, use the `peek` method instead of `value`. |
238 | | - |
239 | | -```ruby |
240 | | -require "signalize" |
241 | | -include Signalize::API |
242 | | - |
243 | | -counter = signal(0) |
244 | | -effect_count = signal(0) |
245 | | - |
246 | | -effect do |
247 | | - puts counter.value |
248 | | - |
249 | | - # Whenever this effect is triggered, increase `effect_count`. |
250 | | - # But we don't want this signal to react to `effect_count` |
251 | | - effect_count.value = effect_count.peek + 1 |
252 | | -end |
253 | | -``` |
254 | | - |
255 | | -## Signalize Struct |
256 | | - |
257 | | -An optional add-on to Signalize, the `Singalize::Struct` class lets you define multiple signal or computed variables to hold in struct-like objects. You can even add custom methods to your classes with a simple DSL. (The API is intentionally similar to `Data` in Ruby 3.2+, although these objects are of course mutable.) |
258 | | - |
259 | | -Here's what it looks like: |
260 | | - |
261 | | -```ruby |
262 | | -require "signalize/struct" |
263 | | - |
264 | | -include Signalize::API |
265 | | - |
266 | | -TestSignalsStruct = Signalize::Struct.define( |
267 | | - :str, |
268 | | - :int, |
269 | | - :multiplied_by_10 |
270 | | -) do # optional block for adding methods |
271 | | - def increment! |
272 | | - self.int += 1 |
273 | | - end |
274 | | -end |
275 | | - |
276 | | -struct = TestSignalsStruct.new( |
277 | | - int: 0, |
278 | | - str: "Hello World", |
279 | | - multiplied_by_10: computed { struct.int * 10 } |
280 | | -) |
281 | | - |
282 | | -effect do |
283 | | - puts struct.multiplied_by_10 # 0 |
284 | | -end |
285 | | - |
286 | | -effect do |
287 | | - puts struct.str # "Hello World" |
288 | | -end |
289 | | - |
290 | | -struct.increment! # above effect will now output 10 |
291 | | -struct.str = "Goodbye!" # above effect will now output "Goodbye!" |
292 | | -``` |
293 | | - |
294 | | -If you ever need to get at the actual `Signal` object underlying a value, just call `*_signal`. For example, you could call `int_signal` for the above example to get a signal object for `int`. |
295 | | - |
296 | | -Signalize structs require all of their members to be present when initializing…you can't pass only some keyword arguments. |
297 | | - |
298 | | -Signalize structs support `to_h` as well as `deconstruct_keys` which is used for pattern matching and syntax like `struct => { str: }` to set local variables. |
299 | | - |
300 | | -You can call `members` (as both object/class methods) to get a list of the value names in the struct. |
301 | | - |
302 | | -Finally, both `inspect` and `to_s` let you debug the contents of a struct. |
303 | | - |
304 | | -## Development |
305 | | - |
306 | | -After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/rake test` to run the tests, or `bin/guard` or run them continuously in watch mode. You can also run `bin/console` for an interactive prompt that will allow you to experiment. |
307 | | - |
308 | | -To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org). |
309 | | - |
310 | | -## Contributing |
311 | | - |
312 | | -Signalize is considered a direct port of the [original Signals JavaScript library](https://github.com/preactjs/signals). This means we are unlikely to accept any additional features other than what's provided by Signals (unless it's completely separate, like our `Signalize::Struct` add-on). If Signals adds new functionality in the future, we will endeavor to replicate it in Signalize. Furthermore, if there's some unwanted behavior in Signalize that's also present in Signals, we are unlikely to modify that behavior. |
313 | | - |
314 | | -However, if you're able to supply a bugfix or performance optimization which will help bring Signalize _more_ into alignment with its Signals counterpart, we will gladly accept your PR! |
315 | | - |
316 | | -This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/whitefusionhq/signalize/blob/main/CODE_OF_CONDUCT.md). |
317 | | - |
318 | | -## License |
319 | | - |
320 | | -The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). |
| 3 | +> [!CAUTION] |
| 4 | +> This project [has migrated to Codeberg](https://codeberg.org/jaredwhite/signalize). Please update your references accordingly and do not link to this GitHub project. It has now been archived. Thank you! |
0 commit comments