Atoms

Atoms are the smallest stable unit of reactivity in Thorm. Everything else builds on them.

A quick origin story

In cosmology, the Big Bang is the moment structure becomes possible. The universe begins as a dense, energetic state, then cools into particles, then atoms, and eventually into complex matter. Atoms are not the whole universe, but they are the smallest stable unit that everything else builds on.

Thorm reactivity follows a similar arc. An atom is a small, stable unit of state. It starts simple, but it is the seed of all behavior: you read it, you derive expressions from it, you bind those expressions to nodes, and suddenly your UI has motion and meaning. Atoms are not your UI — they are the foundation that makes the UI responsive to change.

What an atom is

An atom is a container for state. It holds a value and exposes a stable identity that the runtime can observe. You create atoms with state(), then read them with expressions.

Atoms are not global variables, and they are not the DOM. They are references to data inside the IR tree. This is why you can pass them around, combine them into expressions, and update them via actions without losing track of what should re-render.

Creating atoms

Use state(initial, meta) to create an atom. The initial value can be a number, string, boolean, array, or object. Meta is optional, but it is useful for tooling and debugging.

  • initial: the starting value for the atom
  • meta.name: a human-readable identifier shown in tools/logs
  • meta.scope: a namespace that helps group related atoms

Atoms are cheap. Create them freely for local UI state, but use meta to keep complex screens traceable.

Reading atoms and building expressions

Atoms only become reactive when you read them. read($atom) converts an atom into an expression that can be used in the IR tree.

Expressions are not evaluated in PHP. They are stored in the IR and evaluated by the runtime, which is how changes to atoms update the UI. This is why you typically wrap values in expressions instead of computing them ahead of time.

  • read($atom) - turns an atom into an expression
  • val(x) - wraps a literal as an explicit expression

Derived expressions

Once you can read atoms, you can compose expressions. This is how you format text, branch UI, and derive values without mutating state directly.

  • concat combines literals and expressions into a string
  • cond chooses between expressions based on a condition
  • eq and cmp provide equality and comparison operators
  • mod is useful for parity checks or cyclic UI

Expressions are composable. A derived expression can be used inside text(), in classes, in attributes, or as input to other expressions.

Updating atoms

Atoms change through actions. Actions can be attached to events like click or used inside tasks and effects. The most common update helpers are set(), inc(), and add().

  • set($atom, value) updates an atom to a new value
  • inc($atom, by) increments a numeric atom
  • add($atom, by) can be used as an expression or as an action depending on context

The key idea is that updates are explicit. You are not mutating variables; you are describing an action that the runtime executes. This keeps reactivity consistent and predictable.

Persisting and restoring atoms

When state should survive page reloads, use persist and hydrate. These functions store and restore atom values using URL-like sources such as local://, cookie:// or https://.

Persistence is optional, but it is a powerful pattern for dashboards, preferences, and any UI where the last state should be remembered.

Example: a single atom with derived UI

This counter shows the full data flow. One atom ($cnt) drives multiple expressions: the main count text, a parity check, and a playful message when the count is divisible by 13.

Notice how the UI uses expressions everywhere: read($cnt) is combined with concat(), eq(), and mod() to drive conditional nodes. The button wires an action (inc) to a click event, and the rest of the UI updates automatically.

PHP

<?php
$cnt 
state(1);

$app el('div', [ cls('container mt-5') ], [
    
el('h1', [], [ 
        
text(concat('Count: 'read($cnt))), 
        
show(eq(mod(read($cnt), 2), 0), el('span', [ cls('') ], [ text(' is even') ])),
        
show(eq(mod(read($cnt), 2), 1), el('span', [ cls('') ], [ text(' is odd') ])),
        
show(eq(mod(read($cnt), 13), 0), el('p', [ cls('') ], [ text('How odd is that 🙃?') ])),
    ]),
    
el('button', [ 
        
cls('btn btn-warning'),
        
on('click'inc($cnt1)) 
    ], [ 
text('Inc') ]),
    
show(eq(mod(read($cnt), 2), 0), el('span', [ cls('m-5 fs-5') ], [ text('Even!') ]))
]);

Example: two atoms + a derived sum

Here you have two independent atoms ($a and $b). Each atom has its own set of buttons and its own read() expression in the UI.

The sum line shows how expressions can combine atoms. add($a, read($b)) produces a derived value used inside concat(). This kind of derivation keeps your UI declarative: the sum is always correct because it is not stored separately, it is computed from the current atoms.

PHP

<?php
$a 
state(0); $b state(0);
$app el('div', [ cls('container') ], [
    
el('h2', [], [ text('Two Counters + Sum') ]),
    
el('div', [ cls('my-2') ], [
        
el('div', [ cls('btn-group ') ], [
            
el('button', [ cls('btn btn-primary'), on('click'inc($a, -1))], [ text('A--') ]),
            
el('button', [cls('btn btn-primary'), on('click'inc($a1))], [ text('A++') ]),
        ]), 
        
el('span', [], [ 
            
text(concat(' A = 'read($a))) 
        ]) 
    ]),
    
el('div', [ cls('my-2') ], [ 
        
el('div', [ cls('btn-group ') ], [
            
el('button', [ cls('btn btn-primary'), on('click'inc($b, -1)) ], [ text('B--') ]), 
            
el('button', [ cls('btn btn-primary'), on('click'inc($b1)) ], [ text('B++') ]), 
        ]),
        
el('span', [], [ text(concat(' B = 'read($b))) ]) 
    ]),
    
el('h3', [], [ text(concat('Sum = 'add($aread($b)))) ]),
]);

Practical tips

  • Use atoms for data that changes over time. If it never changes, you can keep it as a literal value or a val() expression.
  • Prefer derived expressions over duplicated state. If a value can be computed from atoms, compute it in the view instead of storing it.
  • Give important atoms a name and scope so debugging and tooling remain clear as your app grows.

Atoms are the bedrock. Once you are comfortable creating, reading, and updating them, the rest of the Thorm API becomes much easier to reason about.