|
| 1 | +# Reactivity Design Notes |
| 2 | + |
| 3 | +Internal reference for the reactive features: `always`, `when`, and `bind`. |
| 4 | + |
| 5 | +## Why this works without explicit signals |
| 6 | + |
| 7 | +SolidJS (and most JS reactive frameworks) use explicit signal primitives: |
| 8 | + |
| 9 | +```js |
| 10 | +const [count, setCount] = createSignal(0); |
| 11 | +count() // read — registers a subscription |
| 12 | +setCount(5) // write — notifies subscribers |
| 13 | +``` |
| 14 | + |
| 15 | +This is necessary because JavaScript has no way to intercept plain variable |
| 16 | +reads and writes. `myVar = 5` is invisible to framework code. So Solid must |
| 17 | +wrap values in function calls to run custom logic on access. |
| 18 | + |
| 19 | +_hyperscript doesn't have this constraint. It is its own language with its own |
| 20 | +execution engine. Every variable read goes through `resolveSymbol()` in the |
| 21 | +runtime, and every write goes through `setSymbol()`. The runtime is already |
| 22 | +the middleman for all variable access, so we hook tracking and notification |
| 23 | +directly into these existing paths: |
| 24 | + |
| 25 | +- **Read** (`resolveSymbol`): if an effect is currently evaluating, record the |
| 26 | + variable as a dependency. |
| 27 | +- **Write** (`setSymbol`): notify all effects that depend on this variable. |
| 28 | + |
| 29 | +The user gets reactive behavior without any API ceremony. `$count` is just a |
| 30 | +variable. Writing `when $count changes ...` is all it takes to make it watched. |
| 31 | + |
| 32 | +## The JS interop boundary |
| 33 | + |
| 34 | +This approach is airtight for pure _hyperscript code, but leaky at the JS |
| 35 | +boundary: |
| 36 | + |
| 37 | +- **Writes from JS** (`window.$count = 99`) bypass `setSymbol`, so no |
| 38 | + notification fires. |
| 39 | +- **Reads inside `js()` blocks** (`js(x) return window.$price * window.$qty end`) |
| 40 | + bypass `resolveSymbol`, so no dependencies are tracked. |
| 41 | +- **`Object.assign` in the js feature** writes directly to `globalScope`, |
| 42 | + also bypassing `setSymbol`. |
| 43 | + |
| 44 | +This is an accepted trade-off. For typical _hyperscript usage the reactivity |
| 45 | +is invisible and correct. Users mixing JS interop with reactive expressions |
| 46 | +should be aware that the tracking boundary is the _hyperscript runtime. |
| 47 | + |
| 48 | +## What creates reactivity |
| 49 | + |
| 50 | +Variables are not signals. `set $count to 0` just stores a value on |
| 51 | +`globalScope` (i.e. `window`). Nothing reactive happens. |
| 52 | + |
| 53 | +Reactivity is created by `always`, `when`, and `bind`. All three use |
| 54 | +`createEffect()` under the hood, which evaluates code with tracking |
| 55 | +enabled. During that evaluation, `resolveSymbol` sees that an effect |
| 56 | +is active and records dependencies. Future writes via `setSymbol` notify |
| 57 | +all subscribed effects. |
| 58 | + |
| 59 | +The variable itself has no idea it's being watched. The reactivity lives |
| 60 | +entirely in the effect and the subscription maps. |
| 61 | + |
| 62 | +## The three reactive features |
| 63 | + |
| 64 | +Each serves a distinct purpose: |
| 65 | + |
| 66 | +- **`always`** declares relationships. The block runs as one unit with |
| 67 | + automatic dependency tracking and re-runs when any dependency changes. |
| 68 | + Used for derived values, DOM updates, and conditional state. For |
| 69 | + independent tracking, use separate `always` statements. |
| 70 | + |
| 71 | +- **`when`** reacts to changes. Watches a specific expression and runs a |
| 72 | + command body when the value changes. `it` refers to the new value. Used |
| 73 | + for side effects, async work, and events. |
| 74 | + |
| 75 | +- **`bind`** syncs two values bidirectionally. Includes same-value dedup to |
| 76 | + prevent infinite loops. Used for form inputs and shared state. |
| 77 | + |
| 78 | +`always` runs its entire command block inside the tracking context (the |
| 79 | +block IS the effect). `when` separates the tracked expression from the |
| 80 | +handler commands. `bind` creates two `when`-style effects pointing at |
| 81 | +each other. |
| 82 | + |
| 83 | +## Why styles and computed styles are not tracked |
| 84 | + |
| 85 | +`*opacity`, `*computed-width`, and other style references are not reactive. |
| 86 | +There is no efficient DOM API for "notify me when a computed style changes." |
| 87 | +`MutationObserver` only catches inline style attribute edits, not changes |
| 88 | +from classes, media queries, CSS animations, or the cascade. No reactive |
| 89 | +framework (SolidJS, Vue, Svelte) tracks computed styles either. |
| 90 | + |
| 91 | +To react to style-affecting changes, track the cause instead: the variable, |
| 92 | +attribute, or class that drives the style. |
0 commit comments